Cet article présente Onyx, notre VM pour l'orchestration programmable d'agents. Et par extension, un runtime qui transforme l'orchestration en ingénierie logicielle. À la fin de cet article, vous comprendrez les contraintes et les décisions de conception qui ont présidé à la construction de la VM, ainsi que la manière de créer vos propres programmes et d'architecturer vos systèmes d'agents.
Introduction
Les agents sont intrinsèquement non déterministes. C'est tout l'intérêt. Si vous vouliez du déterminisme, vous écririez du logiciel.
Mais à un moment donné, tous ceux qui utilisent des agents ont collectivement voulu aller plus loin. Nous avons appris que diviser l'exécution en étapes structurées améliore les performances : Planifier, Implémenter, Réviser, QA, etc. Ensuite, nous avons apparemment convenu d'écrire des scripts, des outils et des compétences pour diriger chaque agent, partager le contexte entre eux et les encadrer. Nous assemblons ensuite ces scripts en redirigeant le texte entre les agents, et comme nous ne faisons que passer du texte, ça fonctionne plus ou moins.
Si vous passiez assez de temps sur le problème et que vous étiez particulièrement astucieux, vous auriez trouvé comment obtenir des garanties de votre système pour pouvoir avoir une exécution conditionnelle basée sur un état donné. Et vous stockeriez probablement cet état dans un fichier de balisage analysable ou un ensemble de fichiers de balisage pour diriger vos scripts bash. Vous auriez peut-être même construit une CLI personnalisée pour que vos agents l'utilisent.
En tant qu'ingénieurs, cela nous est familier, nous utilisons des scripts tout en faisant du génie logiciel. Cependant, les logiciels modernes ne sont pas construits en enchaînant des scripts bash et des outils CLI. Au lieu de cela, nous avons des langages de programmation, des runtimes et des chaînes d'outils pour nous aider à concevoir nos systèmes. Nous écrivons des logiciels avec des langages de programmation parce qu'ils sont fournis avec une bibliothèque standard, une sémantique claire et un modèle d'exécution sur lequel nous pouvons compter. Ils ont des écosystèmes riches avec des chaînes d'outils pour tous nos besoins.
Les garanties qu'ils nous offrent sur nos systèmes nous permettent de raisonner à des niveaux d'abstraction plus élevés.
Mais il n'existe pas d'équivalent pour concevoir des systèmes d'agents. Afin de construire des systèmes, l'orchestration d'agents doit être programmable exactement de la même manière que les logiciels modernes.
Aujourd'hui, nous présentons la spécification des PROGRAMMES (*.program.ts), et Onyx, notre VM conçue pour l'orchestration déterministe d'agents. Cet article explore l'historique de l'orchestration d'agents, la sémantique statique et d'exécution d'une VM capable d'exécuter un programme, et ses implications pour la direction que prend le domaine.
Cela semble coûteux, mais en réalité, ça ne l'est pas. J'explique cela plus tard dans l'article.
Pour les curieux, voici à quoi ressemble l'Autoresearch d'Andrej Karpathy sous forme de programme :

Problèmes non résolus dans l'orchestration d'agents
Pour comprendre ce qu'un runtime pour l'orchestration d'agents devrait inclure, nous devons comprendre les limites des agents.
Un agent LLM peut être considéré comme un générateur de flux JSON, alimenté dans un analyseur, qui envoie ensuite des appels d'outils à un environnement dans une boucle.
Chaque appel d'outil a exactement la même forme de schéma externe, mais le contenu de ce flux de sortie n'est pas déterministe.
La combinaison du déterminisme et du non-déterminisme est ce qui a rendu les agents si précieux. Ils sont suffisamment flexibles pour enchaîner des séquences d'actions de manière unique, mais suffisamment déterministes pour interagir avec un ordinateur via des appels d'outils.
La composabilité est presque gratuite si vous êtes prêt à abandonner l'exigence que le contenu de ce flux soit typé. Les modèles sont suffisamment bons pour rediriger le texte en entrée et en sortie dans les rails que nous leur fournissons : invites, messages et appels d'outils.
Cela expose une interface très composable : le texte
Le texte est une interface universelle. Tout sur un ordinateur peut être sérialisé en texte, même s'il ne s'agit que de code machine. Si vous pouvez avoir une entrée et une sortie de texte LLM via cette interface universelle, vous obtenez une composabilité sur les flux de texte.
Cela signifie que la fiabilité de votre comportement d'agent est directement liée à la cohérence de la sortie du modèle. Une variabilité élevée de la sortie signifie un comportement d'agent plus erratique.
Une fois que vous avez une interface pour composer des éléments, la prochaine contrainte qui vous importe est la dirigeabilité :
ce que vous voulez que l'agent fasse, et comment vous l'amenez systématiquement à faire ce que vous voulez
Nous dirigeons les agents en décalant la distribution à partir de laquelle ils échantillonnent, en d'autres termes, en formulant des invites.
En 2022, ReAct est apparu et a essentiellement été le pionnier de la dirigeabilité des agents. En fait, on peut même dire qu'il a fait exister les agents tels que nous les connaissons. La réflexion et le raisonnement sur le résultat d'un outil avant de passer à l'étape suivante est ce qui maintient la boucle cohérente.

Nous avions toujours besoin que les agents soient plus intelligents. L'utilisation de la mise à l'échelle du calcul au moment du test, industrialisée par la série O de modèles @OpenAI, a donné aux laboratoires de modèles la capacité d'intégrer un meilleur comportement d'agent [[11]](http://localhost:5173/blog/onyx#ref-11). Produire plus de jetons avant d'appeler un outil permet au modèle d'échapper à la distribution de sortie dans laquelle il serait resté bloqué s'il avait été contraint sur la longueur de la sortie de raisonnement. Vous pouvez choisir d'entraîner la façon dont le modèle parcourt son paysage de distribution de sortie, et donc avoir la liberté d'entraîner un comportement d'agent plus clair sur les tâches qui vous intéressent.
À mesure que la longueur du contexte augmente sans limite, diriger l'agent devient difficile et l'accomplissement de la tâche devient moins probable. Même avec un modèle de raisonnement, il n'y a aucune garantie de récupération, et l'agent meurt sur place. L'agent peut atteindre sa limite de contexte, déclarer une fin prématurée, se bloquer dans une boucle, etc.
Extraire des garanties d'un système non déterministe
Les solutions à cela étaient variées, mais l'une d'elles se démarque : La boucle Ralph, créée par @GeoffreyHuntley . [[3]](http://localhost:5173/blog/onyx#ref-3
Il a introduit l'idée que vous pouviez limiter l'exécution de l'agent, puis utiliser ces limites pour raisonner sur l'accomplissement de la tâche. Cela permet à la boucle Ralph de faire quelque chose de magique : elle fournit quelque chose sur lequel vous pouvez compter dans un système non déterministe.
Une étincelle de déterminisme.
Mieux vaut garantir l'échec et progresser progressivement vers quelque chose de correct, que de tirer une fois de plus le levier de la machine à sous. Cette limite définie vous donne quelque chose de concret sur lequel raisonner et, une fois que vous pouvez raisonner sur les limites de quelque chose, vous pouvez en faire un système.
Lutter contre les limites de la longueur du contexte
Il y a un problème cependant : un agent frais perd sa cohérence entre les exécutions, mais un seul agent manque de contexte avec suffisamment de temps.
Entrez le RLM par @lateinteraction @a1zhang. RLM nous a donné un concept pour interagir avec un contexte long (c'est-à-dire une exécution d'agent) de manière structurée [[4]](http://localhost:5173/blog/onyx#ref-4). RLM s'est inspiré de CodeAct, un article de 2024 qui démontrait l'utilisation du code pour orchestrer les opérations [[5]](http://localhost:5173/blog/onyx#ref-5). L'agent écrit des scripts qui orchestrent les opérations dans un REPL pour ensuite récupérer une sortie. RLM fonctionne de la même manière, avec la particularité supplémentaire qu'il utilise des variables pour stocker le contexte et effectuer des opérations sur ce contexte. Il permet également des appels LLM récursifs dans le REPL. Vous pourriez perdre une certaine réactivité que d'autres boucles ont, mais vous gagnez la capacité de travailler programmatiquement avec le contexte. Le point clé ici est que les scripts dans le REPL sont éphémères. Vous obtenez un runtime de script et une gestion de contexte, mais il n'y a ni réutilisabilité ni composabilité. Écrivez simplement le script, exécutez-le, et il disparaît. En termes de construction de systèmes, c'est strictement pire que de simplement enchaîner des agents et des fichiers Markdown avec des scripts bash, car vous perdez la persistance et l'exécution limitée.
Passer des boucles individuelles à l'orchestration à l'échelle
La Deep research d'OpenAI [[6]](http://localhost:5173/blog/onyx#ref-6) a été l'un des premiers exemples d'un flux de travail déterministe qui avait une forme ou un schéma d'exécution général avec une faible variabilité d'une exécution à l'autre. La façon dont cela fonctionne est en planifiant un lot de requêtes, en les exécutant sur le web, en examinant les résultats et en planifiant le lot suivant de requêtes. Chaque lot sondant plus profondément l'espace du problème.

Cursor a poussé l'idée du déterminisme beaucoup plus loin lorsque @wilsonzlin a démontré un harnais qui orchestrait des agents pour construire un navigateur. Il a construit un harnais sur mesure pour coordonner de grandes quantités de travail en utilisant des agents planificateurs parallèles et des agents de tâches [[7]](http://localhost:5173/blog/onyx#ref-7). Ce qui est pertinent ici, c'est que la relation entre chaque partie du harnais est fixe. Il y a des planificateurs, qui explorent l'état actuel du système et génèrent des tâches, et des exécuteurs, qui prennent les tâches et les implémentent en parallèle. Il y a des garde-fous fixes entre les agents et des canaux fixes pour communiquer les informations. Pour bien faire la coordination, vous avez besoin de garanties sur les interfaces.
Utiliser les conditions de terminaison pour une exécution limitée
En mai, Codex a introduit l'idée d'un objectif qui utilise une boucle de vérification pour gravir une colline vers un état final souhaité jusqu'à ce qu'une tâche soit terminée. Vous pouvez considérer cela comme une version prête pour la production de la boucle Ralph, intégrée dans Codex. Cela vous permet de spécifier un objectif, et a une boucle automatisée qui exécute et examine, intégrée.

L'autoresearch de Karpathy [[9]](http://localhost:5173/blog/onyx#ref-9) est similaire à /goal de Codex et à la boucle Ralph. Il combine la condition de terminaison vérifiable de l'objectif avec la limitation d'exécution d'une boucle Ralph sur des itérations, lui permettant de progresser continuellement vers un objectif. Il progresse en explorant l'espace des idées, en s'améliorant de manière itérative au fil du temps.

Jusqu'à présent, toutes les solutions qui externalisent l'orchestration en dehors de l'agent sont fixes dans la forme de leur graphe d'exécution. Elles s'exécutent en utilisant un modèle écrit à la main et ont une sorte de schéma pour les formes autorisées dans lesquelles elles peuvent fonctionner. Elles ne s'adaptent pas par tâche ou n'ont pas de garanties solides sur la forme du graphe d'exécution tout court.
Rendre l'orchestration flexible
En mars de cette année, nous avons présenté Slate, le premier agent de codage à utiliser le code pour l'orchestration en direct de sous-agents dans le style de RLM. C'est toujours le seul agent de codage bien utilisé qui utilise le code pour faire de l'orchestration d'agents en direct. Dans Slate, les threads peuvent être créés, mis en pause, repris et dirigés en temps réel. L'agent principal comprend profondément comment orchestrer tous les sous-agents en cours d'exécution afin que vous n'ayez pas à le faire. Cependant, comme pour RLM, nous étions toujours confrontés au défi du partage d'état entre les sous-agents et des scripts éphémères, ce qui n'est pas quelque chose que vous rencontreriez en utilisant un script bash et un fichier Markdown.
Même ainsi, si c'est le modèle qui fait l'orchestration, comment le diriger ? Lui dites-vous d'écrire son code d'orchestration d'une manière spécifique ? Que faites-vous ?
Notre solution initiale (en tant que correctif avant de publier le runtime Onyx) s'appelait les compétences d'orchestration [[13]](http://localhost:5173/blog/onyx#ref-13). L'idée était simple : permettre à l'utilisateur de fournir une compétence pour diriger la façon dont l'agent aborde son orchestration. C'est tout. Cela fonctionnait correctement, mais cela avait beaucoup de problèmes.
Notamment, une compétence n'est pas un contrat de comportement contraignant. Vous ne pouvez pas obtenir une garantie à partir du texte.
Cela signifie que l'orchestrateur n'était pas obligé de suivre le modèle d'exécution souhaité car il n'y avait aucun moyen réel de l'appliquer. L'un des plus grands avantages du runtime Onyx est que nous avons résolu ce problème.
Aucun des systèmes mentionnés n'a de contrats de comportement contraignants.
Eh bien, alors, et si l'agent pouvait écrire son code d'orchestration dans un script par tâche afin que le graphe d'exécution soit fixe ? C'est ce que sont les flux de travail dynamiques 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 même manière que RLM et Slate, en écrivant du code pour orchestrer les sous-agents, les flux de travail dynamiques permettent à Claude d'écrire et de sauvegarder des formes de flux de travail. Cela se combine avec /loop pour pouvoir boucler sur des modèles spécifiques. Cela fournit un contrat déclaratif pour le comportement d'un ensemble d'agents. Ce n'est toujours pas la même chose que d'écrire un logiciel car cela manque de choses comme la composition fonctionnelle, mais vous obtenez une persistance et une garantie solide sur la façon dont la tâche sera exécutée. Ce sont des scripts de flux de travail écrits dynamiquement pour une tâche donnée, de manière ad hoc.[[12]](http://localhost:5173/blog/onyx#ref-12) Et comme ils sont persistés sur le disque, ils ont un avantage supplémentaire : ils peuvent être réexécutés et enveloppés avec une colle d'orchestration comme /loop.

Si vous remarquez, toutes les solutions ci-dessus visent la même chose : un moyen déterministe de contrôler la façon dont les agents s'exécutent dans le temps.
C'est une histoire que nous avons déjà vue se dérouler dans le domaine du génie logiciel. Nous avons commencé par assembler des systèmes disparates et des tâches de script, puis nos langages sont devenus plus flexibles et plus puissants. Nous avons gagné de plus en plus de levier sur le processus d'ingénierie avec des écosystèmes plus solides nous permettant de construire des systèmes plus fiables à des niveaux d'abstraction plus élevés.
Actuellement, les agents sont sur la même trajectoire, et aujourd'hui nous publions la prochaine étape de cette trajectoire pour vous permettre de concevoir les systèmes qui exécutent vos agents. Les langages de programmation utilisent souvent des interpréteurs ou des VM pour planifier automatiquement les ressources. C'est ce qui vous donne du levier en tant qu'ingénieur utilisant le langage.
Pour qu'une VM ait un sens pour l'orchestration d'agents, vous auriez besoin de quelques éléments :
- Gestion d'état persistante : nous devrions pouvoir définir un état, le référencer par son nom, le persister et le manipuler par programmation.
- Garanties de type. Nous devrions respecter les formes d'entrée et de sortie définies, les suivre et pouvoir compter sur elles.
- Primitives de flux de contrôle, de préférence bien connues qu'un LLM comprendrait.
- Structure claire pour la gestion des erreurs (par exemple, try-catch).
- Gestion des ressources : contrôles définis sur les ressources comme le parallélisme des agents, le coût, les modèles en cours d'exécution, etc.
- Isolation d'exécution : un agent ou un programme en cours d'exécution doit être isolé d'un autre, à moins que l'état ne soit explicitement partagé.
- Contrôle du cycle de vie : à quoi ressemble un programme d'agent et la sémantique pour l'exécuter, l'annuler et le diriger. Sans cela, vous n'avez pas de chemin clair pour le nettoyage et vous ne pouvez pas contrôler la gestion du cycle de vie.
- Composabilité : les programmes doivent pouvoir se composer les uns dans les autres et être appelables avec des types d'entrée et de sortie définis.
- Visibilité : nous devrions pouvoir savoir ce qui a été exécuté, quand, et être capables de retracer un échec d'exécution dans la source.
- Durabilité : nous devrions avoir un modèle clair pour la façon dont nous pouvons récupérer des plantages et reprendre.
Chacun de ces problèmes a déjà été résolu par les langages de programmation il y a des décennies. L'orchestration d'agents les rencontre tous à nouveau pour la première fois.
Pour pouvoir vraiment écrire un logiciel pour cela, un programme "program.ts" doit être créé dans un runtime qui supporte tout ce qui précède, afin que nous puissions raisonner sur ce qui se passera lorsqu'un programme ne fonctionne pas et concevoir une solution autour de l'échec.
C'est pourquoi nous avons construit Onyx. C'est une VM d'orchestration d'agents conçue précisément pour supporter à la fois des programmes persistants et composables et une couche de script interprété. Voici comment cela fonctionne, et ce qu'un runtime compatible "program.ts" doit supporter.
Conception du runtime
Lorsque nous concevons un langage et un runtime pour ce langage, nous devons penser aux contraintes sur lesquelles nous voulons pouvoir raisonner, et à ce qui nous importe d'être facilement exprimable. Ensuite, nous pouvons diviser la sémantique résultante en deux catégories : la sémantique statique et la sémantique d'exécution.
La sémantique statique regroupe toutes les choses qui peuvent être déduites d'un programme simplement en le regardant. Les choses qu'un compilateur ou un vérificateur de type sait sur un programme donné.
La sémantique d'exécution définit ce que le code signifie réellement et comment le programme s'exécute réellement. Cela inclut l'allocation des ressources sous-jacentes et les mécanismes de planification.
Notre objectif avec un runtime pour agents est de transformer le flux de contrôle d'orchestration en code, et nous voulons rendre l'état d'exécution persistant et typé afin de pouvoir l'utiliser de manière fiable pour diriger l'orchestration.
Quelques exigences de la VM
Il y a 3 choses spécifiques à la VM qui nous importent au-delà de quelque chose comme une exécution TypeScript normale.
- En tant que runtime d'orchestration d'agents, il doit être capable d'orchestrer des agents. Cela signifie les créer, suivre leurs cycles de vie, etc. Nous voulons que le runtime soit capable de les exécuter de manière bloquante ou non bloquante et de les planifier correctement.
- Nous voulons contrôler les formes de sortie des agents et voulons une application stricte du contrat de sortie.
- Nous voulons avoir un contrôle d'exécution sur les ressources externes comme les modèles et le coût.
Exécution d'agents et de programmes
Pour exécuter un agent, nous avons sélectionné deux verbes de base : run et spawn. Run exécute un agent bloquant au premier plan. Spawn exécute un agent en arrière-plan. Cela est conforme aux compréhensions courantes de spawn, comme posix_spawn, ce qui permet à un modèle de comprendre facilement nos nouveaux verbes car ils sont conceptuellement dans les données d'entraînement. Spawn et run vous permettent d'invoquer directement des agents et des programmes lus depuis le disque, renvoyant suffisamment d'informations pour un handle d'exécution.
Run supporte également quelques éléments. Il supporte les types de sortie directement appliqués via zod @colinhacks, et il supporte les remplacements de modèle directs, ce qui facilite l'écriture et l'exécution de programmes où il est logique de répartir le travail sur plusieurs modèles différents pour différentes solutions ou différentes étapes d'une tâche.
1function run<S extends z.ZodType>(2 name: string,3 options: ...4): Promise<z.infer<S>>
Run vous permet d'enchaîner directement des sous-agents en ligne.
1// exécution d'agent simple2const out = await run({ type: "read", prompt: () => "Répondre avec : ok" })3// exécution nommée (string = workflowId enfant)4const review = await run("reviewer", {5 type: "general",6 prompt: () => "Examiner le diff",7})8// sortie structurée (résultat typé)9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })10const v = await run({11 type: "general",12 prompt: () => "Évaluer le risque",13 output: Verdict,14})
Spawn est similaire à run mais crée un agent en arrière-plan. Les sous-agents créés avec spawn ne sont pas attendus et le flux de contrôle continue. Spawn est très utile pour lancer plusieurs agents d'exécution non bloquants.
1// agent en arrière-plan2const h = await spawn("worker", { type: "general", prompt: "Tâche longue" })
Interagir avec les agents en cours d'exécution
Nous voulons pouvoir effectuer deux types d'opérations sur les agents en cours d'exécution : la direction et l'arrêt.
Un message de direction est un message envoyé à l'agent que le LLM recevra pendant son exécution pour le pousser dans une direction. Ceci est utile pour mettre à jour le contexte de la tâche de l'agent sans avoir à démanteler le worker.
L'annulation est également importante, nous voulons pouvoir démanteler activement un sous-agent s'il ne devrait pas être en cours d'exécution.
Pouvoir exécuter ces opérations à la fois depuis le REPL en direct et depuis un programme pré-écrit donne à Slate sa capacité à orchestrer tout en temps réel. Il peut définir dynamiquement la forme de l'orchestration au moment de l'exécution, ou il peut écrire et itérer sur un logiciel réel pour faire l'orchestration.
Slate est capable d'écrire des programmes dans des fichiers \.program.ts. Un fichier programme a quelques éléments : son nom (c'est ainsi que Slate sait ce que c'est), une description JSDoc, puis le corps du programme lui-même*. Une déclaration de programme ressemble à ceci :
1program(async (ctx) => {2 // modèle bon marché pour la recherche — il a juste besoin de trouver des fichiers3 const findings = await run("search", {4 type: "read",5 prompt: "Trouver tous les fichiers liés à l'authentification",6 model: "codex/gpt-4.1-mini", // utilise votre clé codex intégrée7 })8})
Les programmes suivent le même modèle d'exécution asynchrone, ce qui nous permet d'exécuter un programme à la fois au premier plan et en arrière-plan, et d'interagir avec lui pendant son exécution.
1// agent en arrière-plan2const h = await spawn("worker", { type: "general", prompt: "Tâche longue" })3await h.notify("concentrez-vous d'abord sur l'analyseur") // message de direction vers l'agent en cours d'exécution4const result = await h.result() // attendre la fin plus tard5// répartir, puis rassembler6const a = await spawn({ prompt: "tâche A" })7const b = await spawn({ prompt: "tâche B" })8const [ra, rb] = [await a.result(), await b.result()]9// exécuter un programme en arrière-plan10import Audit from "deep-audit"11const ah = await spawn(Audit, { input: { pr: 42 } })12const auditResult = await ah.result()
Sortie structurée et état
C'est une limitation majeure de tous les autres systèmes à ce jour. L'état, dans tous les autres systèmes, est mal externalisé et n'est pas isolé en toute sécurité. S'il s'agit d'un fichier sur le système, vous ne pouvez pas garantir qu'il n'y aura pas de corruption. Si vous le pouvez, vous ne pouvez toujours pas garantir l'analysabilité. Vous ne pouvez pas vous abonner aux changements d'état pour piloter les opérations, et vous ne pouvez pas garantir l'adhésion au type.
Vous vous souvenez comment nous voulions un état persistant qui soit également structuré et référençable ?
L'état, dans Onyx, est différent. Les espaces de noms d'état sont déclarés, directement nommés et persistés dans le temps. Cela signifie qu'un magasin d'état peut être réutilisé encore et encore, vous permettant de construire des systèmes d'agents à longue durée de vie avec de vraies données.
Les agents et le code lisent l'état, et le déterminisme que nous voulions d'un runtime découle de cela. Les agents lisent l'état via un outil dédié qui leur permet d'interagir toujours avec lui de manière structurée et sécurisée. Les agents et les programmes sont tous deux des consommateurs qui peuvent être dirigés pour modifier l'état, ce qui permet au runtime de s'appuyer sur l'objet d'état pour piloter l'orchestration.
L'état et l'adhésion au schéma conditionnent l'achèvement du sous-agent. De ce fait, l'état fournit une surface unifiée pour diriger l'ensemble du programme.
Les objets d'état peuvent également être passés comme variables d'exécution vers le bas dans les sessions enfants partagées avec l'agent principal. Cet accès par référence dans toute la hiérarchie des agents (qui est une première en son genre) permet une communication inter-agents via un canal d'état partagé.

Boucles à longue durée de vie
Certains programmes doivent fonctionner davantage comme des systèmes en cours d'exécution. Prenez openclaw par exemple. Vous pouvez en fait représenter openclaw comme un programme étant donné les bonnes primitives. Pour cela, nous utilisons deux primitives : sleep et checkpoint.
Sleep fait ce à quoi vous vous attendez, il dort.
Maintenant, voici le problème : disons que vous voulez une gestion de tâches à longue durée de vie en arrière-plan. Un graphe d'exécution prédéfini pourrait se bloquer ou se casser, et il est donc important que l'agent principal soit au courant du statut du programme.
Pour supporter cela, nous introduisons la primitive checkpoint.
Un checkpoint peut être n'importe quoi, mais la raison pour laquelle il est nommé checkpoint est qu'il notifie l'agent principal avec un objet de forme fixe. Cela permet à l'agent principal de suivre des choses comme la progression des tâches et d'être notifié directement des changements dans l'état du programme. En retour, l'agent principal peut alors gérer plus efficacement un programme en cours d'exécution.
Onyx supporte la création d'une boucle d'agent comme Openclaw, c'est-à-dire un agent persistant avec un battement de cœur.
C'est vraiment impressionnant : vous pouvez composer les primitives pour créer un type d'agent complètement différent en utilisant simplement une boucle while, un sleep et un point de contrôle.
Openclaw peut simplement être représenté comme un fichier de programme !
1// Un programme pour exécuter une boucle de recherche automatique de longue durée2for (let i = 0; i < maxExperiments; i++) {3 const idea = await run("proposer", { ... })4 const result = await run("entraîner", { ... })5 checkpoint({ message: `expérience ${i}`, data: { idea, result } })6 await sleep(30_000) // temps de refroidissement entre les expériences7}89// Un programme pour exécuter un agent persistant de type openclaw10while(true) {11 const status = await run("vérification_statut", { ...insérer modèle peu coûteux ici... })if(status.tâches_en_attente) {checkpoint({ tâches: status.tâches_en_attente }) // retourner l'état important et réveiller l'agent principal}12 await sleep(30_000) // temps de refroidissement entre les expériences13}
Composition
Avec Onyx, Slate peut écrire un *.program.ts pour vous. Ce fichier persiste et peut (et doit) être traité comme du code normal. Il dispose de types intégrés, s'exécute dans un environnement d'exécution dépourvu de variables globales, et comme c'est du TypeScript, son modèle de composition consiste simplement à importer et appeler un autre programme.
Comme c'est du TypeScript, vous bénéficiez gratuitement de fonctionnalités comme le parallélisme (Promise.all) et les boucles.
Voici comment importer un programme et l'utiliser dans un autre :
1import Audit depuis "audit-approfondi"programme (() => {const ah = await spawn(Audit, { input: { pr: 42 } })2 const résultatAudit = await ah.resultat()3 const correcteur = await run("correcteur", ... sortie audit) // ceci exécuterait et corrigerait la sortie du programme d'audit.4})
Sémantique des erreurs
Les erreurs, dans la machine virtuelle idéale, sont levées de manière explicite. Elles doivent être levées en cas de problèmes de syntaxe à l'exécution, d'échecs d'agents, de plantages, etc.
Plus précisément, nous définissons les erreurs d'orchestration comme suit :
- Un agent est bloqué sur une tâche
- Un agent n'a pas réussi à terminer une tâche
- Un agent a épuisé le nombre d'étapes ou le budget pour une tâche
- Un programme a épuisé le budget pour une exécution
- Le modèle d'orchestration n'a pas réussi à écrire du code syntaxiquement correct
- Une modification d'état illégale a été effectuée
Tous ces cas d'erreur spécifiques définissent la sémantique d'exécution. Ils signifient : « Vous pouvez vous attendre à ce que cet environnement d'exécution lève une erreur, car nous considérons un échec d'exécution d'agent de la même manière qu'une erreur dans le code ». Cela peut sembler agaçant au début, mais ce mécanisme d'échec explicite vous offre quelque chose en retour : un moyen explicite de vous préparer et de programmer autour des échecs. En réalité, cela vous donne donc plus de contrôle, pas moins.
1// les erreurs sont gérées par try/catch — comme dans tout programme TypeScript2programme(async (ctx) => {3 try {4 const resultat = await run("refactorisation-risquée", {5 type: "général",6 prompt: "Refactoriser le module d'authentification",7 modèle: "claude-sonnet",8 étapesMax: 20,9 })10 } catch (err) {11 // l'agent a échoué — mais nous savons exactement pourquoi.12 // la trace contient chaque appel d'outil, chaque requête de modèle,13 // chaque écriture d'état qui a mené ici.1415 // réessayer avec un modèle différent16 const resultat = await run("refactorisation-risquée-nouvel-essai", {17 type: "général",18 prompt: `La tentative précédente a échoué : ${err.message}. Essayez une approche différente.`,19 modèle: "claude-opus",20 étapesMax: 30,21 })22 }23})
Sélection du modèle, application du budget et BYOK
La sélection intégrée du modèle vous permet d'avoir un contrôle encore plus précis. La compétence /models donne à Slate un accès complet à la liste des modèles disponibles, permettant à Slate de créer des programmes avec plusieurs modèles différents effectuant différentes tâches. Vous voulez que Fable soit le planificateur, mais que GLM 5.2 implémente dans un environnement déterministe ? Pas de problème. Vous voulez diffuser une question entre Gemini, GPT 5.5 et DeepSeek ? Cela fonctionne aussi.
De plus, l'environnement d'exécution prend en charge deux types de remplacements de configuration pour les programmes :
- Les modèles globaux par défaut utilisés pour l'exécution des agents
- Le budget pour exécuter un programme
Vous pouvez définir directement un budget d'exécution pour plafonner les dépenses pour une boucle donnée.
De plus, l'environnement d'exécution prend en charge l'utilisation de vos abonnements existants à OpenAI et Github Copilot.
1programme(async (ctx) => {2 // modèle peu coûteux pour la recherche — il a juste besoin de trouver des fichiers3 const résultats = await run("recherche", {4 type: "lecture",5 prompt: "Trouver tous les fichiers liés à l'authentification",6 modèle: "codex/gpt-4.1-mini", // utilise votre clé codex intégrée7 })89 // modèle de raisonnement pour la partie difficile — il a besoin de réfléchir10 const plan = await run("architecte", {11 type: "général",12 prompt: `Concevoir un correctif basé sur : ${résultats.sortie}`,13 modèle: "openai/o3", // Utilise les crédits API14 sortie: z.object({15 approche: z.string(),16 fichiers: z.array(z.string()),17 risque: z.enum(["faible", "moyen", "élevé"]),18 }),19 })2021 // modèle de niveau intermédiaire pour l'implémentation — il a juste besoin d'éditer22 const handles = await Promise.all(23 plan.fichiers.map(f => spawn("corriger-" + f, {24 type: "général",25 prompt: `Appliquer ce correctif à ${f} : ${plan.approche}`,26 modèle: "anthropic/claude-sonnet-5",27 étapesMax: 15,28 }))29 )30 await Promise.all(handles.map(h => h.résultat()))31})
Définir la surface de création
Deux facteurs principaux ont guidé la conception de la surface de création des programmes : la facilité avec laquelle un agent peut la comprendre, et la facilité avec laquelle un humain peut la lire. Nous avons choisi des verbes relativement simples qui se lisent comme de l'anglais, et nous avons explicitement décidé de modéliser l'orchestration de manière procédurale plutôt que déclarative.
Le choix de TypeScript comme langage était également important. Il y a tellement de code TypeScript procédural dans la nature qu'un modèle comprendra implicitement la sémantique de TypeScript, même sans post-entraînement.

Pièces d'ingénierie de notre usine logicielle
La question suivante à laquelle il faut répondre est : qu'est-ce que tout cela vous apporte ?
Cela vous apporte la capacité d'écrire un véritable logiciel pour votre orchestration d'agents. Vous pouvez désormais concevoir votre propre orchestration d'agents de bout en bout.
Vous pouvez concevoir l'usine.
Par exemple, vous pouvez créer un programme qui surveille Github en boucle, et un programme séparé qui exécute un agent d'implémentation avec un agent QA pour la révision. Deux modèles individuellement utiles que vous pourriez rencontrer dans la nature. Ensuite, vous pouvez les assembler pour créer un système qui écoute les commentaires sur une PR, génère un implémenteur pour répondre à ces commentaires, puis génère un agent QA pour s'assurer que le correctif est valide.
Vous pouvez ensuite utiliser ce programme connecté à une file d'attente de tâches pour déléguer et surveiller le travail sur votre codebase, et lui faire répondre automatiquement aux commentaires de PR.
Et vous pouvez faire tout cela en utilisant des modèles open weights rapides. Parce que ce n'est que du code, vous n'avez pas besoin d'un LLM puissant pour réfléchir à l'orchestration après sa première création.
Passons maintenant à la partie amusante, il est temps de partager certains des programmes que nous avons utilisés pour des augmentations massives de productivité.
Recherche Approfondie sur le Codebase
Nous utilisons ce programme pour aider à définir le périmètre des tâches. Il effectue des recherches approfondies sur l'état de notre monorepo et prépare un dossier de recherche pour qu'un implémenteur puisse le consulter. Nous l'utilisons tout le temps. Cela semble coûteux, mais en réalité, ça ne l'est pas. Vous pouvez exécuter ce programme dans Slate avec DeepSeek V4 Flash et le processus de recherche est approfondi mais extrêmement bon marché.

Objectif-Révision-PR
Nous utilisons celui-ci pour implémenter une tâche une fois la recherche terminée. Heureusement, au moment où la recherche atteint le programme d'objectif, la plupart de l'ambiguïté de la tâche a été résolue, ce qui rend l'exécution de la tâche encore plus rapide. Charger la recherche en amont avec un modèle OSS léger nous permet d'utiliser un modèle coûteux comme Opus pour ce qui compte : écrire du code réellement bon et vérifier l'état du système. Vous pourriez même modifier le programme pour utiliser GPT 5.5 afin de réviser de manière antagoniste le travail d'Opus 4.8.

Autoresearch en tant que Programme
Autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9) était à l'origine entièrement piloté par LLM. Dirigez un agent vers l'invite program.md et il décide quoi essayer et comment progresser.
Sans surprise, Autoresearch est en fait juste un programme.
Les programmes d'agents vous permettent d'inverser cela et de placer le flux de contrôle dans l'environnement d'exécution. Le programme possède le flux de contrôle tandis que les agents effectuent le travail avec effets secondaires (éditer du code, exécuter git, se connecter en SSH au GPU distant, s'entraîner). Pour le programme autoresearch, la décision de conserver/revenir en arrière est un code déterministe :
1conservé = statut === "ok" && valBpb != null && valBpb < meilleur
Dans notre cas, le programme exécute un agent de configuration pour préparer un nouveau dépôt et vérifier que l'A100 distant est accessible. Si la configuration échoue, il revient tôt avec une sortie propre basée sur une valeur typée. Sinon, il entre dans la boucle d'expériences.
Chaque expérience obtient un nouvel agent. L'agent reçoit la meilleure configuration actuelle et l'historique des idées et résultats précédents, afin de ne pas se répéter et de pouvoir s'appuyer sur ce qui a été conservé. Il propose un changement, édite train.py, commit, synchronise avec la machine distante, s'entraîne et classifie le résultat.
L'agent et le programme partagent l'état. L'agent écrit des données dans l'état, et le programme évalue l'état pour le flux de contrôle. En fonction du résultat, un agent enregistreur met à jour results.tsv, et réinitialise éventuellement l'exécution si le programme a décidé de jeter l'expérience. Cela laisse la HEAD de git toujours pointant vers la meilleure branche actuelle de l'arbre d'expériences.
Il y a deux différences fondamentales à noter : 1) cela s'exécute dans un programme, donc nous pouvons générer un nouvel agent par expérience et 2) nous pouvons décider quelle tâche l'agent doit effectuer en fonction de l'état en direct du programme.


Et voici à quoi cela ressemble en code :
1// ---------- Programme ----------23programme(async (ctx) => {4 const c = cfg(ctx.entrée)5 const total = ctx.entrée?.maxExpériences ?? 2067 const configuration = await run("ar-configuration", {8 prompt: inviteConfiguration(c),9 type: "général",10 étapesMax: 40,11 sortie: RésultatConfiguration,12 })13 if (!configuration.prêt) {14 return { abandonné: true, raison: `échec de la configuration : ${configuration.note}`, configuration }15 }1617 let meilleur = c.valeurBpbBase18 let meilleurCommit = configuration.commitBase19 const historique = []2021 for (let i = 1; i <= total; i++) {22 let exp23 try {24 exp = await run(`ar-exp-${i}`, {25 prompt: inviteExpérience(c, i, total, meilleur, texteHistorique(historique)),26 type: "général",27 étapesMax: 80,28 sortie: RésultatExpérience,29 })30 } catch (err) {31 // L'agent a rencontré une erreur/a été bloqué — traiter comme un plantage, restaurer le dépôt au meilleur, continuer.32 exp = {33 description: `expérience ${i} erreur agent`,34 commit: "erreur",35 statut: "plantage",36 valBpb: null,37 picVramMb: null,38 nombreÉtapes: null,39 codeSortie: -1,40 tentatives: 0,41 note: String(err?.message ?? err).slice(0, 200),42 }43 }4445 const conservé = exp.statut === "ok" && exp.valBpb != null && exp.valBpb < meilleur4647 await run(`ar-enregistrer-${i}`, {48 prompt: inviteEnregistrement(c, exp, conservé, meilleurCommit),49 type: "général",50 étapesMax: 20,51 sortie: RésultatEnregistrement,52 })5354 if (conservé) {55 meilleur = exp.valBpb56 meilleurCommit = exp.commit57 }5859 historique.push({60 idx: i,61 description: exp.description,62 statut: exp.statut,63 valBpb: exp.valBpb,64 conservé,65 commit: exp.commit,66 tentatives: exp.tentatives,67 })6869 await checkpoint({70 nom: `expérience-${i}`,71 message: `exp ${i}/${total}: ${exp.statut}${conservé ? " CONSERVÉ" : ""} val_bpb=${exp.valBpb ?? "n/a"} (meilleur=${meilleur})`,72 données: { i, total, statut: exp.statut, valBpb: exp.valBpb, conservé, meilleur, meilleurCommit },73 })74 }7576 const conservés = historique.filter((h) => h.conservé)77 return {78 valeurBpbBase: c.valeurBpbBase,79 meilleureValeurBpb: meilleur,80 meilleurCommit,81 amélioration: c.valeurBpbBase - meilleur,82 expériencesRéalisées: historique.length,83 conservés: conservés.length,84 plantages: historique.filter((h) => h.statut === "plantage").length,85 échecsInfra: historique.filter((h) => h.statut === "échec_infra").length,86 dépôtLocal: c.dépôtLocal,87 branche: c.branche,88 historique,89 }90})
Travaux futurs
La seule exigence restante de la machine virtuelle que nous n'avons pas encore définie est le modèle de durabilité pour les programmes. Il n'est pas clair quel est le modèle correct pour reprendre et gérer le cycle de vie d'un programme, et quel niveau de contrôle doit être exposé sur l'environnement d'exécution.
Au-delà de cela, il y a tellement de choses passionnantes que nous allons ajouter pour prendre en charge différentes charges de travail et formes de tâches, afin que nous puissions écrire de vrais logiciels pour orchestrer les agents plus efficacement. Nous sommes certains que de nombreux modèles émergeront de l'utilisation créative des programmes par les utilisateurs eux-mêmes.
Nous avons hâte de voir ce que vous allez construire.
- L'équipe RL
Références
- Yao et al., "ReAct : Synergiser le Raisonnement et l'Action dans les Modèles de Langage," 2022
- Geoffrey Huntley, "La Boucle Ralph"
- Geoffrey Huntley, "tout est une boucle ralph," Janvier 2026
- Zhang, Kraska, Khattab, "Modèles de Langage Récursifs," Décembre 2025
- Wang et al., "Les Actions de Code Exécutable Améliorent les Agents LLM," ICML 2024
- OpenAI, "Introduction à la Recherche Approfondie," Février 2025
- Cursor, "Mise à l'Échelle des Agents," Janvier 2026
- OpenAI, "Utiliser les Objectifs dans Codex"
- Andrej Karpathy, "autoresearch"
- Anthropic, "Introduction des Workflows Dynamiques dans Claude Code"
- OpenAI, "Apprendre à Raisonner avec les LLMs," Septembre 2024
- Anthropic, "Un harnais pour chaque tâche : workflows dynamiques dans Claude Code"
- Random Labs, "Chaînage de Compétences"





