Este artículo presenta Onyx, nuestra máquina virtual para orquestación programable de agentes. Y, por extensión, un runtime que convierte la orquestación en ingeniería de software. Al final de este artículo, comprenderás las restricciones y decisiones de diseño que se tomaron para construir la VM, así como cómo crear tus propios programas y arquitecturar tus sistemas de agentes.
Introducción
Los agentes son inherentemente no deterministas. Ese es el punto. Si quisieras determinismo, estarías escribiendo software.
Pero en algún momento, todos los que usan agentes quisieron llevarlos más lejos. Aprendimos que dividir la ejecución en pasos estructurados ayuda al rendimiento: Planificar, Implementar, Revisar, QA, etc. Luego, aparentemente acordamos escribir scripts, herramientas y habilidades para dirigir cada agente, compartir contexto entre ellos y establecer barreras de seguridad. Luego, unimos estos scripts canalizando texto entre agentes, y como solo pasamos texto, más o menos funciona.
Si dedicabas suficiente tiempo al problema y eras particularmente ingenioso, habrías descubierto cómo obtener garantías de tu sistema para poder tener ejecución condicional basada en un estado dado. Y probablemente almacenarías ese estado en un archivo de marcado analizable o en un conjunto de archivos de marcado para dirigir tus scripts de bash. Incluso podrías haber creado una CLI personalizada para que la usen tus agentes.
Como ingenieros, esto nos resulta familiar: usamos scripts mientras hacemos ingeniería de software. Sin embargo, el software moderno no se construye encadenando scripts de bash y herramientas de CLI. En cambio, tenemos lenguajes de programación, runtimes y cadenas de herramientas que nos ayudan a ingenierizar nuestros sistemas. Escribimos software con lenguajes de programación porque vienen con una biblioteca estándar, semántica clara y un modelo de ejecución en el que podemos confiar. Tienen ecosistemas ricos con cadenas de herramientas para todas nuestras necesidades.
Las garantías que nos brindan sobre nuestros sistemas nos permiten razonar en niveles más altos de abstracción.
Pero no existe un equivalente para la ingeniería de sistemas de agentes. Para construir sistemas, la orquestación de agentes debe ser programable exactamente de la misma manera que el software moderno.
Hoy, presentamos la especificación para PROGRAMS (*.program.ts), y Onyx, nuestra VM construida para la orquestación determinista de agentes. Este artículo explora una historia de la orquestación de agentes, la semántica estática y de runtime de una VM que puede ejecutar un programa, y sus implicaciones para hacia dónde se dirige el campo.
Suena caro, pero en realidad no lo es. Explico esto más adelante en el artículo.
Para los curiosos, así es como se ve el Autoresearch de Andrej Karpathy como un programa:

Problemas no resueltos en la orquestación de agentes
Para entender qué debería incluir un runtime para la orquestación de agentes, necesitamos entender las limitaciones de los agentes.
Un agente LLM puede considerarse como un generador de flujo JSON, alimentado a un analizador, que luego envía llamadas a herramientas a un entorno en un bucle.
Cada llamada a herramienta tiene exactamente la misma forma de esquema externo, pero el contenido de este flujo de salida no es determinista.
La combinación de determinismo y no determinismo es lo que ha hecho que los agentes sean tan valiosos. Son lo suficientemente flexibles para encadenar secuencias de acciones de formas únicas, pero lo suficientemente deterministas para interactuar con una computadora a través de llamadas a herramientas.
La componibilidad es casi gratuita si estás dispuesto a renunciar al requisito de que el contenido de ese flujo esté tipado. Los modelos son lo suficientemente buenos para canalizar texto de entrada y salida dentro de los rieles que les proporcionamos: prompts, mensajes y llamadas a herramientas.
Esto expone una interfaz muy componible: texto
El texto es una interfaz universal. Todo en una computadora se puede serializar a texto, incluso si es solo código de máquina. Si puedes hacer que un LLM ingrese y genere texto a través de esta interfaz universal, obtienes componibilidad sobre flujos de texto.
Esto significa que la confiabilidad de tu comportamiento del agente está directamente relacionada con la consistencia de la salida del modelo. Una alta variabilidad en la salida significa un comportamiento del agente más errático.
Una vez que tienes una interfaz para componer piezas, la siguiente restricción que te importa es la dirigibilidad** :
lo que quieres que haga el agente, y cómo logras consistentemente que haga lo que quieres
Dirigimos agentes cambiando la distribución de la que muestrea, en otras palabras, prompting.
En 2022, ReAct surgió y esencialmente fue pionero en la dirigibilidad de los agentes. De hecho, podemos llegar a decir que hizo que los agentes tal como los conocemos fueran una realidad. Pensar y razonar sobre la salida de una herramienta antes de dar el siguiente paso es lo que mantiene el bucle coherente.

Todavía necesitábamos que los agentes fueran más inteligentes. El uso del escalado de cómputo en tiempo de prueba, industrializado por los modelos de la serie O de @OpenAI, les dio a los laboratorios de modelos la capacidad de incorporar un mejor comportamiento de agente [[11]](http://localhost:5173/blog/onyx#ref-11). Generar más tokens antes de llamar a una herramienta permite que el modelo escape de la distribución de salida en la que se habría quedado atascado si se hubiera visto limitado en la longitud del razonamiento de salida. Puedes elegir entrenar cómo el modelo recorre su panorama de distribución de salida y, por lo tanto, tener la libertad de entrenar un comportamiento de agente más claro en las tareas que te importan.
A medida que la longitud del contexto crece sin límites, dirigir al agente se vuelve difícil y la finalización de la tarea se vuelve menos probable. Incluso con un modelo de razonamiento, no hay garantía de recuperación, y el agente muere allí mismo. El agente puede alcanzar su límite de contexto, declarar una finalización temprana, quedarse atascado en un bucle, etc.
Obteniendo garantías de un sistema no determinista
Las soluciones a esto fueron variadas, pero una destaca: El Bucle Ralph, creado por @GeoffreyHuntley. [[3]](http://localhost:5173/blog/onyx#ref-3
Introdujo la idea de que podías acotar la ejecución del agente y luego usar esos límites para razonar sobre la finalización de la tarea. Esto permite que el Bucle Ralph haga algo mágico: proporciona algo en lo que puedes confiar en un sistema no determinista.
Una chispa de determinismo.
Es mejor garantizar el fracaso y avanzar progresivamente hacia algo correcto, que tirar de la palanca de la máquina tragamonedas una vez más. Este límite definido te da algo concreto sobre lo que razonar y, una vez que puedes razonar sobre los límites de algo, puedes construir un sistema a partir de ello.
Luchando contra los límites de la longitud del contexto
Hay un problema, sin embargo: un agente nuevo pierde coherencia entre ejecuciones, pero un solo agente se queda sin contexto con el tiempo suficiente.
Aquí entra RLM de @lateinteraction @a1zhang. RLM nos dio un concepto sobre cómo interactuar con un contexto largo (es decir, una ejecución de agente) de manera estructurada [[4]](http://localhost:5173/blog/onyx#ref-4). RLM se inspiró en CodeAct, un artículo de 2024 que demostró el uso de código para orquestar operaciones [[5]](http://localhost:5173/blog/onyx#ref-5). El agente escribe scripts que orquestan operaciones dentro de un REPL para luego recuperar una salida. RLM opera de la misma manera con la advertencia adicional de que usa variables para almacenar contexto y realizar operaciones en ese contexto. Además, permite llamadas recursivas al LLM en el REPL. Puedes perder algo de la reactividad que tienen otros bucles, pero ganas la capacidad de trabajar programáticamente con el contexto. Lo clave aquí es que los scripts en el REPL son efímeros. Obtienes un runtime de scripting y gestión de contexto, pero no hay reutilización ni componibilidad. Simplemente escribe el script, ejecútalo y desaparece. En términos de construir sistemas, esto es estrictamente peor que simplemente encadenar agentes y archivos markdown con scripts de bash porque pierdes persistencia y ejecución acotada.
Pasando de bucles individuales a escalar la orquestación
Deep research de OpenAI[[6]](http://localhost:5173/blog/onyx#ref-6) fue uno de los primeros ejemplos de un flujo de trabajo determinista que tenía una forma o esquema de ejecución general con pequeña variabilidad en cada ejecución. La forma en que funciona es planificando un lote de consultas, ejecutándolas en la web, revisando los resultados y planificando el siguiente lote de consultas. Cada lote profundiza más en el espacio del problema.

Cursor llevó la idea del determinismo mucho más lejos cuando @wilsonzlin demostró un arnés que orquestaba agentes para construir un navegador. Construyó un arnés a medida para coordinar grandes cantidades de trabajo utilizando agentes planificadores paralelos y agentes de tareas [[7]](http://localhost:5173/blog/onyx#ref-7). Lo relevante aquí es que la relación entre cada parte del arnés es fija. Hay planificadores, que exploran el estado actual del sistema y generan tareas, y hay ejecutores, que toman las tareas y las implementan en paralelo. Hay barreras de seguridad fijas entre agentes y canales fijos para comunicar información. Para hacer una buena coordinación, necesitas garantías en las interfaces.
Usando condiciones de terminación para la ejecución acotada
En mayo, Codex introdujo la idea de un objetivo que utiliza un bucle verificador para escalar colina arriba hacia algún estado final deseado hasta que una tarea esté completa. Puedes pensar en esto como una versión lista para producción del bucle Ralph, integrada en Codex. Te permite especificar un objetivo y tiene un bucle automatizado que ejecuta y revisa, integrado.

El autoresearch de Karpathy[[9]](http://localhost:5173/blog/onyx#ref-9) es similar a /goal de Codex y al bucle Ralph. Combina la condición de terminación verificable de goal con la delimitación de ejecución de un bucle Ralph a lo largo de iteraciones, lo que le permite avanzar continuamente hacia un objetivo. Progresa buscando en el espacio de ideas, mejorando iterativamente con el tiempo.

Hasta este punto, todas las soluciones que externalizan la orquestación fuera del agente tienen una forma de grafo de ejecución fija. Se ejecutan usando un patrón escrito a mano y tienen una especie de esquema para las formas permitidas en las que pueden operar. No se adaptan por tarea o no tienen garantías sólidas sobre la forma del grafo de ejecución en absoluto.
Haciendo la orquestación flexible
En marzo de este año, presentamos Slate, el primer agente de codificación en usar código para la orquestación en vivo de subagentes al estilo de RLM. Sigue siendo el único agente de codificación bien utilizado que usa código para hacer orquestación de agentes en vivo. En Slate, los hilos se pueden generar, pausar, reanudar y dirigir en tiempo real. El agente principal comprende profundamente cómo orquestar todos los subagentes en ejecución para que tú no tengas que hacerlo. Sin embargo, similar a RLM, todavía nos enfrentábamos al desafío de compartir estado entre subagentes y scripting efímero, que no es algo que te encuentres usando un script de bash y un archivo markdown.
Aun así, si el modelo es el que hace la orquestación, ¿cómo lo diriges? ¿Le dices que escriba su código de orquestación de una manera específica? ¿Qué haces?
Nuestra solución inicial (como un parche antes de lanzar el runtime de Onyx) se llamó habilidades de orquestación [[13]](http://localhost:5173/blog/onyx#ref-13). La idea era simple: permitir que el usuario proporcionara una habilidad para dirigir cómo el agente aborda su orquestación. Eso es todo. Funcionaba más o menos bien, pero tenía muchos problemas.
Es decir, una habilidad no es un contrato de comportamiento vinculante. No puedes obtener una garantía del texto.
Esto significa que el orquestador no tenía que seguir el patrón de ejecución deseado porque no había una forma real de hacerlo cumplir. Uno de los mayores beneficios del runtime de Onyx es que resolvimos este problema.
Ninguno de los sistemas mencionados tiene contratos de comportamiento vinculantes.
Bueno, entonces, ¿y si el agente pudiera escribir su código de orquestación en un script por tarea para que el grafo de ejecución esté fijo? Esto es lo que son los flujos de trabajo dinámicos de Claude.[[10]]([http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12](http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12) De la misma manera que RLM y Slate, al escribir código para orquestar subagentes, los flujos de trabajo dinámicos permiten a Claude escribir y guardar formas de flujo de trabajo. Esto se combina con /loop para poder iterar sobre patrones específicos. Proporciona un contrato declarativo para el comportamiento de un conjunto de agentes. Sigue sin ser lo mismo que escribir software, ya que carece de cosas como la composición funcional, pero obtienes persistencia y una garantía sólida sobre cómo se ejecutará la tarea. Son scripts de flujo de trabajo escritos dinámicamente para una tarea determinada ad hoc.[[12]](http://localhost:5173/blog/onyx#ref-12) Y como se persisten en el disco, tienen un beneficio adicional: se pueden volver a ejecutar y envolver con pegamento de orquestación como /loop.

Si te das cuenta, todas las soluciones anteriores buscan lo mismo: una forma determinista de controlar cómo se ejecutan los agentes a lo largo del tiempo.
Esta es una historia que ya vimos desarrollarse en el campo de la ingeniería de software. Comenzamos uniendo sistemas dispares y trabajos de scripting, y luego nuestros lenguajes se volvieron más flexibles y potentes. Obtuvimos cada vez más apalancamiento sobre el proceso de ingeniería con ecosistemas más fuertes que nos permitieron construir sistemas más confiables en niveles de abstracción más altos.
Ahora mismo, los agentes están en la misma trayectoria, y hoy estamos lanzando el siguiente paso en esa trayectoria para permitirte ingenierizar los sistemas que ejecutan tus agentes. Los lenguajes de programación a menudo usan intérpretes o VMs para programar recursos automáticamente. Esto es lo que te da apalancamiento como ingeniero que usa el lenguaje.
Si una VM tuviera sentido para la orquestación de agentes, necesitarías algunas cosas:
- Gestión de estado persistente: deberíamos poder definir estado, referenciarlo por nombre, persistirlo y manipularlo programáticamente.
- Garantías de tipo. Deberíamos respetar las formas de entrada y salida definidas y seguirlas, y poder confiar en ellas.
- Primitivas de flujo de control, preferiblemente bien conocidas para que un LLM las entienda.
- Estructura clara para el manejo de errores (por ejemplo, try-catch).
- Gestión de recursos: controles definidos sobre recursos como el paralelismo de agentes, el costo, qué modelos se están ejecutando, etc.
- Aislamiento de ejecución: un agente o programa en ejecución debe estar aislado de otro a menos que el estado se comparta explícitamente.
- Control del ciclo de vida: cómo se ve un programa de agente y la semántica para ejecutar, cancelar y dirigir. Sin esto, no tienes un camino claro para la limpieza y no puedes controlar la gestión del ciclo de vida.
- Componibilidad: los programas deben poder componerse entre sí y deben ser invocables con tipos de entrada y salida definidos.
- Visibilidad: Deberíamos poder saber qué se ejecutó, cuándo, y deberíamos poder rastrear un fallo de ejecución hasta la fuente.
- Durabilidad: Deberíamos tener un modelo claro de cómo podemos recuperarnos de fallos y reanudar.
Cada uno de estos es un problema que ya fue resuelto por los lenguajes de programación hace décadas. La orquestación de agentes simplemente los está encontrando todos de nuevo por primera vez.
Para poder escribir software para esto, un programa "program.ts" debe ser creado en un runtime que soporte todo lo anterior, para que podamos razonar sobre lo que sucederá cuando un programa no funcione e ingenierizar alrededor del fallo.
Por eso construimos Onyx. Es una VM de orquestación de agentes diseñada precisamente para soportar tanto programas persistentes y componibles como una capa de scripting interpretada. Así es como funciona, y lo que un runtime compatible con "program.ts" necesita soportar.
Diseñando el runtime
Cuando diseñamos un lenguaje y un runtime para ese lenguaje, necesitamos pensar en las restricciones sobre las que queremos poder razonar y en lo que nos importa que sea fácilmente expresable. Luego podemos dividir la semántica resultante en dos categorías: semántica estática y semántica de runtime.
La semántica estática son todas las cosas que se pueden inferir sobre un programa con solo mirarlo. Las cosas que un compilador o un verificador de tipos saben sobre un programa dado.
La semántica de runtime define lo que el código realmente significa y cómo se ejecuta realmente el programa. Esto incluye la asignación de recursos subyacente y la mecánica de programación.
Nuestro objetivo con un runtime para agentes es convertir el flujo de control de la orquestación en código, y queremos que el estado de ejecución sea persistente y esté tipado para que podamos usarlo de manera confiable para dirigir la orquestación.
Algunos requisitos de la VM
Hay 3 cosas específicas de la VM que nos importan más allá de algo como la ejecución normal de TypeScript.
- Como runtime de orquestación de agentes, necesita poder orquestar agentes. Esto significa crearlos, rastrear sus ciclos de vida, etc. Queremos que el runtime pueda ejecutarlos de manera bloqueante o no bloqueante y programarlos correctamente.
- Queremos control sobre las formas de salida de los agentes y queremos una aplicación estricta del contrato de salida.
- Queremos tener control en tiempo de ejecución sobre recursos externos como modelos y costos.
Ejecutando agentes y programas
Para ejecutar un agente, seleccionamos dos verbos básicos: run y spawn. Run ejecuta un agente bloqueante en primer plano. Spawn ejecuta un agente en segundo plano. Esto está en línea con las comprensiones comunes de spawn, como posix_spawn, lo que facilita que un modelo entienda nuestros nuevos verbos, ya que conceptualmente están en los datos de entrenamiento. Spawn y run te permiten invocar directamente agentes y programas leídos del disco, devolviendo suficiente información para un identificador de ejecución.
Run también soporta algunas cosas. Soporta tipos de salida directamente aplicados a través de zod @colinhacks, y soporta anulaciones directas de modelos, lo que facilita escribir y ejecutar programas donde tiene sentido abrirse en abanico a múltiples modelos diferentes para diferentes soluciones o diferentes pasos de una tarea.
1function run<S extends z.ZodType>(2 name: string,3 options: ...4): Promise<z.infer<S>>
Run te permite encadenar directamente subagentes en línea.
1// ejecución de agente simple2const out = await run({ type: "read", prompt: () => "Responder con: ok" })3// ejecución nombrada (string = workflowId hijo)4const review = await run("reviewer", {5 type: "general",6 prompt: () => "Revisar el diff",7})8// salida estructurada (resultado tipado)9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })10const v = await run({11 type: "general",12 prompt: () => "Evaluar riesgo",13 output: Verdict,14})
Spawn es similar a run pero crea un agente en segundo plano. Los subagentes generados con spawn no se esperan y el flujo de control continúa. Spawn es muy útil para iniciar varios agentes de ejecución no bloqueante.
1// agente en segundo plano2const h = await spawn("worker", { type: "general", prompt: "Tarea larga" })
Interactuando con agentes en ejecución
Queremos poder hacer dos tipos de operaciones en agentes en ejecución: dirigir y detener.
Un mensaje de dirección es un mensaje enviado al agente que el LLM recibirá mientras se ejecuta para empujarlo en una dirección. Esto es útil para actualizar el contexto de la tarea del agente sin necesidad de destruir el worker.
La cancelación también es importante; queremos poder eliminar activamente un subagente si no debería estar ejecutándose.
Poder ejecutar estas operaciones tanto desde el REPL en vivo como desde un programa predefinido le da a Slate su capacidad de orquestar todo en tiempo real. Puede definir dinámicamente la forma de la orquestación en tiempo de ejecución, o puede crear e iterar sobre software real para hacer la orquestación.
Slate puede escribir programas en archivos \.program.ts. Un archivo de programa tiene algunas cosas: su nombre (así es como Slate sabe qué es), una descripción JSDoc y luego el cuerpo del programa real*. Una declaración de programa se ve así:
1program(async (ctx) => {2 // modelo barato para búsqueda — solo necesita encontrar archivos3 const findings = await run("search", {4 type: "read",5 prompt: "Encontrar todos los archivos relacionados con autenticación",6 model: "codex/gpt-4.1-mini", // usa tu clave codex integrada7 })8})
Los programas siguen el mismo modelo de ejecución asíncrona, lo que nos permite ejecutar un programa tanto en primer plano como en segundo plano, e interactuar con él mientras se ejecuta.
1// agente en segundo plano2const h = await spawn("worker", { type: "general", prompt: "Tarea larga" })3await h.notify("concéntrate en el analizador primero") // mensaje de dirección al agente en ejecución4const result = await h.result() // esperar finalización más tarde// abrir en abanico, luego recoger5const a = await spawn({ prompt: "tarea A" })6const b = await spawn({ prompt: "tarea B" })7const [ra, rb] = [await a.result(), await b.result()]// programa en segundo plano8import Audit from "deep-audit"9const ah = await spawn(Audit, { input: { pr: 42 } })10const auditResult = await ah.result()
Salida estructurada y estado
Esta es una limitación principal de todos los demás sistemas hasta la fecha. El estado, en todos los demás sistemas, está mal externalizado y no está aislado de forma segura. Si es un archivo en el sistema, no puedes garantizar que no haya corrupción. Si puedes, aún no puedes garantizar la capacidad de análisis. No puedes suscribirte a cambios de estado para impulsar operaciones, y no puedes garantizar la adherencia al tipo.
¿Recuerdas cómo queríamos un estado persistente que también estuviera estructurado y pudiera ser referenciado?
El estado, en Onyx, es diferente. Los espacios de nombres de estado se declaran, se nombran directamente y se persisten a lo largo del tiempo. Esto significa que un almacén de estado se puede reutilizar una y otra vez, permitiéndote construir sistemas de agentes de larga duración con datos reales.
Tanto los agentes como el código leen el estado, y el determinismo que queríamos de un runtime surge de esto. Los agentes leen el estado a través de una herramienta dedicada que les permite interactuar siempre con él de una manera segura y estructurada. Los agentes y los programas son consumidores que pueden ser dirigidos para modificar el estado, lo que permite que el runtime confíe en el objeto de estado para impulsar la orquestación.
El estado y la adherencia al esquema controlan la finalización del subagente. Debido a esto, el estado proporciona una superficie unificada para dirigir todo el programa.
Los objetos de estado también se pueden pasar como variables de runtime a sesiones secundarias compartidas con el agente principal. Este acceso por referencia en toda la jerarquía de agentes (que es el primero de su tipo) permite la comunicación entre agentes a través de un canal de estado compartido.

Bucles de larga duración
Algunos programas necesitan funcionar más como sistemas en ejecución. Toma openclaw, por ejemplo. En realidad, puedes representar openclaw como un programa dadas las primitivas correctas. Para esto, usamos dos primitivas: sleep y checkpoint.
Sleep hace lo que esperarías: duerme.
Ahora, aquí está el asunto: digamos que quieres alguna gestión de tareas de larga duración en segundo plano. Un grafo de ejecución predefinido podría atascarse o romperse, por lo que es importante que el agente principal esté al tanto del estado del programa.
Para soportar esto, introducimos la primitiva checkpoint.
Un checkpoint puede ser cualquier cosa, pero la razón por la que se llama checkpoint es porque notifica al agente principal con un objeto de forma fija. Esto permite que el agente principal rastree cosas como el progreso de la tarea y sea notificado directamente sobre los cambios en el estado del programa. A su vez, el agente principal puede entonces gestionar de manera más efectiva un programa en ejecución.
Onyx soporta hacer un bucle de agente como Openclaw, es decir, un agente persistente con un latido.
Esto es realmente genial: puedes componer las primitivas en un tipo de agente completamente diferente solo usando un bucle while, un sleep y un checkpoint.
¡Openclaw puede representarse simplemente como un archivo de programa!
1// Un programa para ejecutar un bucle de auto-investigación de larga duración2for (let i = 0; i < maxExperiments; i++) {3 const idea = await run("propose", { ... })4 const result = await run("train", { ... })5 checkpoint({ message: `experimento ${i}`, data: { idea, result } })6 await sleep(30_000) // tiempo de espera entre experimentos7}89// Un programa para ejecutar un agente persistente estilo openclaw10while(true) {11 const status = await run("status_check", { ...insertar modelo barato aquí... })if(status.pending_tasks) {checkpoint({ tasks: status.pending_tasks }) // devuelve el estado importante y despierta al agente principal}12 await sleep(30_000) // tiempo de espera entre experimentos13}
Composición
Con Onyx, Slate puede escribir un *.program.ts por ti. Esto persiste y puede (y debe) tratarse como código normal. Viene con tipos incorporados, se ejecuta en un runtime sin globales de runtime, y es simplemente TypeScript, por lo que su modelo de composición es importar y llamar a otro programa.
Como es solo TypeScript, obtienes cosas como paralelismo (Promise.all) y bucles de forma gratuita.
Así es como importarías un programa y lo usarías en otro:
1import Audit from "deep-audit"program (() => {const ah = await spawn(Audit, { input: { pr: 42 } })2 const auditResult = await ah.result()3 const fixer = await run("fixer", ... salida de auditoría) // esto ejecutaría y corregiría la salida del programa de auditoría.4})
Semántica de errores
Los errores, en la VM ideal, se lanzan de forma ruidosa. Estos deben lanzarse en problemas de sintaxis en tiempo de ejecución, fallos de agentes, caídas, etc.
Específicamente, definimos los errores de orquestación como:
- Un agente está bloqueado en una tarea
- Un agente no pudo completar una tarea
- Un agente se quedó sin pasos o presupuesto para una tarea
- Un programa se quedó sin presupuesto para una ejecución
- El modelo de orquestación no pudo escribir código sintácticamente correcto
- Se realizó una modificación de estado ilegal
Todos estos casos de error específicos definen la semántica del runtime. Dicen: "Puedes esperar que este runtime lance un error, porque vemos un fallo en la ejecución del agente de la misma manera que vemos un error en el código". Puede parecer molesto al principio, pero este mecanismo de fallo ruidoso te da algo a cambio: una forma explícita de prepararte y programar en torno a los fallos. Así que, en realidad, te da más control, no menos.
1// los errores son try/catch — igual que cualquier programa TypeScript2program(async (ctx) => {3 try {4 const result = await run("risky-refactor", {5 type: "general",6 prompt: "Refactoriza el módulo de autenticación",7 model: "claude-sonnet",8 maxSteps: 20,9 })10 } catch (err) {11 // el agente falló — pero sabemos exactamente por qué.12 // el trace tiene cada llamada a herramienta, cada solicitud al modelo,13 // cada escritura de estado que llevó hasta aquí.1415 // reintentar con un modelo diferente16 const result = await run("risky-refactor-retry", {17 type: "general",18 prompt: `El intento anterior falló: ${err.message}. Prueba un enfoque diferente.`,19 model: "claude-opus",20 maxSteps: 30,21 })22 }23})
Selección de modelo, control de presupuesto y BYOK
La selección de modelo integrada te permite tener un control aún más preciso. La habilidad /models le da a Slate acceso completo a la lista de modelos disponibles, permitiendo que Slate cree programas con múltiples modelos diferentes realizando distintos trabajos. ¿Quieres que Fable sea el planificador, pero que GLM 5.2 implemente dentro de un entorno determinista? Claro. ¿Quieres distribuir una pregunta entre Gemini, GPT 5.5 y DeepSeek? Eso también funciona.
Además, el runtime admite dos tipos de anulaciones de configuración para los programas:
- Los modelos globales predeterminados utilizados para la ejecución del agente
- El presupuesto para ejecutar un programa
Puedes establecer directamente un presupuesto de ejecución para limitar el gasto de un bucle determinado.
Además, el runtime admite el uso de tus suscripciones existentes de OpenAI y Github Copilot.
1program(async (ctx) => {2 // modelo barato para búsqueda — solo necesita encontrar archivos3 const findings = await run("search", {4 type: "read",5 prompt: "Encuentra todos los archivos relacionados con la autenticación",6 model: "codex/gpt-4.1-mini", // usa tu clave codex integrada7 })89 // modelo de razonamiento para la parte difícil — necesita pensar10 const plan = await run("architect", {11 type: "general",12 prompt: `Diseña una solución basada en: ${findings.output}`,13 model: "openai/o3", // Termina usando créditos de la API14 output: z.object({15 approach: z.string(),16 files: z.array(z.string()),17 risk: z.enum(["low", "medium", "high"]),18 }),19 })2021 // modelo de nivel medio para la implementación — solo necesita editar22 const handles = await Promise.all(23 plan.files.map(f => spawn("fix-" + f, {24 type: "general",25 prompt: `Aplica esta solución a ${f}: ${plan.approach}`,26 model: "anthropic/claude-sonnet-5",27 maxSteps: 15,28 }))29 )30 await Promise.all(handles.map(h => h.result()))31})
Definiendo la superficie de autoría
Hubo dos factores principales en el diseño de la superficie de autoría para los programas: lo fácil que es para un agente entenderlo y lo fácil que es para un humano leerlo. Elegimos verbos relativamente simples que se leen como inglés y decidimos explícitamente que queríamos modelar la orquestación de forma procedural en lugar de declarativa.
La selección de TypeScript como lenguaje también fue importante. Hay tanto código TypeScript procedural en el mundo que un modelo entenderá implícitamente la semántica de TypeScript, incluso sin post-entrenamiento.

Piezas de ingeniería de nuestra fábrica de software
La siguiente pregunta a responder es: ¿qué te aporta todo esto?
Te aporta la capacidad de escribir software real para tu orquestación de agentes. Ahora puedes diseñar tu propia orquestación de agentes de principio a fin.
Puedes diseñar la fábrica.
Por ejemplo, puedes crear un programa que monitoree Github en un bucle y un programa separado que ejecute un agente de implementación con un agente de QA para revisión. Ambos son patrones individualmente útiles que podrías encontrar en el mundo real. Luego puedes combinarlos para hacer un sistema que escuche comentarios en un PR, genere un implementador para abordar esos comentarios y luego genere un agente de QA para asegurarse de que la solución sea válida.
Luego puedes usar este programa conectado a una cola de tareas para delegar y monitorear el trabajo en tu código base, y hacer que responda automáticamente a los comentarios de los PR.
Y puedes hacer todo esto usando modelos de pesos abiertos rápidos. Como es solo código, no necesitas un LLM potente para pensar en la orquestación después de que se haya creado la primera vez.
Ahora viene la parte divertida: es hora de compartir algunos de los programas que hemos utilizado para obtener aumentos masivos de producción.
Investigación Profunda del Código Base
Usamos este programa para ayudar a definir el alcance de las tareas. Realiza una investigación profunda sobre el estado de nuestro monorepo y prepara un paquete de investigación para que un implementador lo consulte. Lo usamos todo el tiempo. Suena caro, pero en realidad no lo es. Puedes ejecutar este programa en Slate con DeepSeek V4 Flash y el proceso de investigación es exhaustivo pero extremadamente barato.

Objetivo-Revisión-PR
Este es el que usamos para implementar una tarea una vez que la investigación está hecha. Afortunadamente, para cuando la investigación llega al programa de objetivos, la mayor parte de la ambigüedad de la tarea se ha resuelto, lo que hace que ejecutar la tarea sea aún más rápido. Cargar la investigación por adelantado con un modelo OSS ligero facilita el uso de un modelo caro como Opus para lo que importa: escribir código realmente bueno y verificar el estado del sistema. Incluso podrías modificar el programa para usar GPT 5.5 y revisar de forma adversaria el trabajo de Opus 4.8.

Autoinvestigación como Programa
La autoinvestigación[[9]](http://localhost:5173/blog/onyx#ref-9) originalmente estaba completamente impulsada por LLM. Dirige un agente al prompt program.md y él decide qué probar y cómo progresar.
Como era de esperar, la autoinvestigación es en realidad solo un programa.
Los programas de agente te permiten invertir eso y poner el flujo de control en el runtime. El programa posee el flujo de control mientras que los agentes hacen el trabajo con efectos secundarios (editar código, ejecutar git, hacer SSH a la GPU remota, entrenar). Para el programa de autoinvestigación, la decisión de mantener/revertir es código determinista:
1kept = status === "ok" && valBpb != null && valBpb < best
En nuestro caso, el programa ejecuta un agente de configuración para preparar un repo nuevo y verificar que la A100 remota sea accesible. Si la configuración falla, regresa temprano con una salida limpia basada en un valor tipado. De lo contrario, entra en el bucle de experimentos.
Cada experimento obtiene un agente nuevo. Al agente se le da la mejor configuración actual y el historial de ideas y resultados anteriores, para que no se repita y pueda basarse en lo que se mantuvo. Propone un cambio, edita train.py, hace commit, rsync a la máquina remota, entrena y clasifica el resultado.
El agente y el programa comparten estado. El agente escribe datos en el estado, y el programa evalúa el estado para el flujo de control. Según el resultado, un agente registrador actualiza results.tsv y, opcionalmente, reinicia la ejecución si el programa decidió descartar el experimento. Esto deja el HEAD de git apuntando siempre a la mejor rama actual del árbol de experimentos.
Hay dos diferencias clave que vale la pena señalar: 1) esto se ejecuta en un programa, por lo que podemos generar un agente nuevo por experimento, y 2) podemos decidir qué tarea debe hacer el agente según el estado del programa en vivo.


Y se ve así en código:
1// ---------- Programa ----------23program(async (ctx) => {4 const c = cfg(ctx.input)5 const total = ctx.input?.maxExperiments ?? 2067 const setup = await run("ar-setup", {8 prompt: setupPrompt(c),9 type: "general",10 maxSteps: 40,11 output: SetupResult,12 })13 if (!setup.ready) {14 return { aborted: true, reason: `configuración fallida: ${setup.note}`, setup }15 }1617 let best = c.baselineValBpb18 let bestCommit = setup.baselineCommit19 const history = []2021 for (let i = 1; i <= total; i++) {22 let exp23 try {24 exp = await run(`ar-exp-${i}`, {25 prompt: experimentPrompt(c, i, total, best, historyText(history)),26 type: "general",27 maxSteps: 80,28 output: ExperimentResult,29 })30 } catch (err) {31 // Error/bloqueo del agente — tratar como un fallo, restaurar repo al mejor, continuar.32 exp = {33 description: `experimento ${i} error de agente`,34 commit: "error",35 status: "crash",36 valBpb: null,37 peakVramMb: null,38 numSteps: null,39 exitCode: -1,40 retries: 0,41 note: String(err?.message ?? err).slice(0, 200),42 }43 }4445 const kept = exp.status === "ok" && exp.valBpb != null && exp.valBpb < best4647 await run(`ar-record-${i}`, {48 prompt: recordPrompt(c, exp, kept, bestCommit),49 type: "general",50 maxSteps: 20,51 output: RecordResult,52 })5354 if (kept) {55 best = exp.valBpb56 bestCommit = exp.commit57 }5859 history.push({60 idx: i,61 description: exp.description,62 status: exp.status,63 valBpb: exp.valBpb,64 kept,65 commit: exp.commit,66 retries: exp.retries,67 })6869 await checkpoint({70 name: `experimento-${i}`,71 message: `exp ${i}/${total}: ${exp.status}${kept ? " MANTENIDO" : ""} val_bpb=${exp.valBpb ?? "n/a"} (mejor=${best})`,72 data: { i, total, status: exp.status, valBpb: exp.valBpb, kept, best, bestCommit },73 })74 }7576 const kepts = history.filter((h) => h.kept)77 return {78 baselineValBpb: c.baselineValBpb,79 bestValBpb: best,80 bestCommit,81 improvement: c.baselineValBpb - best,82 experimentsRun: history.length,83 kept: kepts.length,84 crashes: history.filter((h) => h.status === "crash").length,85 infraFails: history.filter((h) => h.status === "infra_fail").length,86 localRepo: c.localRepo,87 branch: c.branch,88 history,89 }90})
Trabajo futuro
El único requisito restante de la VM que aún no hemos definido es el modelo de durabilidad para los programas. No está claro cuál es el modelo correcto para reanudar y manejar el ciclo de vida de un programa, y qué nivel de control debería exponerse en el runtime.
Más allá de eso, hay tantas cosas emocionantes que agregaremos para soportar diferentes cargas de trabajo y formas de tareas, para que podamos escribir software real para orquestar agentes de manera más efectiva. Estamos seguros de que muchos de los patrones surgirán de personas que usen los programas de formas creativas por su cuenta.
Estamos ansiosos por ver lo que construirás.
- El equipo de RL
Referencias
- Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
- Geoffrey Huntley, "The Ralph Loop"
- Geoffrey Huntley, "everything is a ralph loop," January 2026
- Zhang, Kraska, Khattab, "Recursive Language Models," December 2025
- Wang et al., "Executable Code Actions Elicit Better LLM Agents," ICML 2024
- OpenAI, "Introducing Deep Research," February 2025
- Cursor, "Scaling Agents," January 2026
- OpenAI, "Using Goals in Codex"
- Andrej Karpathy, "autoresearch"
- Anthropic, "Introducing Dynamic Workflows in Claude Code"
- OpenAI, "Learning to Reason with LLMs," September 2024
- Anthropic, "A harness for every task: dynamic workflows in Claude Code"
- Random Labs, "Skill Chaining"





