Dieser Artikel stellt Onyx vor, unsere VM für programmierbare Agenten-Orchestrierung. Und damit eine Laufzeitumgebung, die Orchestrierung in Softwareentwicklung verwandelt. Am Ende dieses Artikels wirst du die Einschränkungen und Designentscheidungen verstehen, die in den Bau der VM eingeflossen sind, sowie lernen, wie du eigene Programme erstellst und deine Agentensysteme architektonisch gestaltest.
Einführung
Agenten sind von Natur aus nicht-deterministisch. Das ist der springende Punkt. Wenn du Determinismus wolltest, würdest du Software schreiben.
Aber irgendwann wollten alle, die Agenten nutzen, sie gemeinsam weiterbringen. Wir haben gelernt, dass die Aufteilung der Ausführung in strukturierte Schritte die Leistung verbessert: Planen, Implementieren, Überprüfen, Qualitätssicherung usw. Dann haben wir uns scheinbar darauf geeinigt, Skripte, Tools und Fähigkeiten zu schreiben, um jeden Agenten zu steuern, Kontext zwischen ihnen zu teilen und sie abzusichern. Wir flicken diese Skripte dann zusammen, indem wir Text zwischen Agenten hin- und herleiten, und weil wir nur Text weitergeben, funktioniert es irgendwie.
Wenn du genug Zeit mit dem Problem verbracht und besonders clever warst, hast du vielleicht herausgefunden, wie du Garantien aus deinem System herausholen kannst, um eine bedingte Ausführung basierend auf einem bestimmten Zustand zu ermöglichen. Und du würdest diesen Zustand wahrscheinlich in einer analysierbaren Auszeichnungsdatei oder einer Reihe von Auszeichnungsdateien speichern, um deine Bash-Skripte zu steuern. Du hast vielleicht sogar eine benutzerdefinierte CLI für deine Agenten gebaut.
Als Ingenieure ist uns das vertraut, wir verwenden Skripte während der Softwareentwicklung. Moderne Software wird jedoch nicht durch das Verketten von Bash-Skripten und CLI-Tools gebaut. Stattdessen haben wir Programmiersprachen, Laufzeitumgebungen und Toolchains, die uns helfen, unsere Systeme zu entwickeln. Wir schreiben Software mit Programmiersprachen, weil sie mit einer Standardbibliothek, klaren Semantiken und einem Ausführungsmodell kommen, auf das wir uns verlassen können. Sie haben reichhaltige Ökosysteme mit Toolchains für all unsere Bedürfnisse.
Die Garantien, die sie uns über unsere Systeme geben, erlauben es uns, auf höheren Abstraktionsebenen zu denken.
Aber es gibt kein Äquivalent für die Entwicklung von Agentensystemen. Um Systeme zu bauen, muss die Agenten-Orchestrierung auf die gleiche Weise programmierbar sein wie moderne Software.
Heute stellen wir die Spezifikation für PROGRAMS (*.program.ts) und Onyx, unsere für deterministische Agenten-Orchestrierung gebaute VM, vor. Dieser Artikel untersucht eine Geschichte der Agenten-Orchestrierung, die statischen und Laufzeitsemantiken einer VM, die ein Programm ausführen kann, und ihre Auswirkungen darauf, wohin sich das Feld entwickelt.
Es klingt teuer, ist es aber nicht. Ich erkläre das später im Artikel.
Für die Neugierigen: So sieht Andrej Karpathys Autoresearch als Programm aus:

Ungelöste Probleme in der Agenten-Orchestrierung
Um zu verstehen, was eine Laufzeitumgebung für die Agenten-Orchestrierung beinhalten sollte, müssen wir die Grenzen von Agenten verstehen.
Ein LLM-Agent kann man sich als einen JSON-Stream-Generator vorstellen, der in einen Parser eingespeist wird, der dann in einer Schleife Tool-Aufrufe an eine Umgebung sendet.
Jeder Tool-Aufruf hat die exakt gleiche äußere Schemaform, aber der Inhalt dieses Ausgabestroms ist nicht deterministisch.
Die Kombination aus Determinismus und Nicht-Determinismus ist das, was Agenten so wertvoll gemacht hat. Sie sind flexibel genug, um Aktionssequenzen auf einzigartige Weise zu verketten, aber deterministisch genug, um durch Tool-Aufrufe mit einem Computer zu interagieren.
Komponierbarkeit ist fast kostenlos, wenn du bereit bist, die Anforderung fallen zu lassen, dass der Inhalt dieses Streams typisiert sein muss. Modelle sind gut genug, um Text innerhalb der Schienen, die wir ihnen vorgeben, ein- und auszuleiten: Prompts, Nachrichten und Tool-Aufrufe.
Dies legt eine sehr komponierbare Schnittstelle offen: Text
Text ist eine universelle Schnittstelle. Alles auf einem Computer kann zu Text serialisiert werden, selbst wenn es nur Maschinencode ist. Wenn du einen LLM-Eingang und -Ausgang durch diese universelle Schnittstelle haben kannst, erhältst du Komponierbarkeit über Textströme.
Das bedeutet, dass die Zuverlässigkeit deines Agentenverhaltens in direktem Zusammenhang mit der Konsistenz der Ausgabe des Modells steht. Eine hohe Ausgabevariabilität bedeutet ein unberechenbareres Agentenverhalten.
Sobald du eine Schnittstelle hast, um Teile durchzukomponieren, ist die nächste Einschränkung, die dir wichtig ist, die Steuerbarkeit:
was du willst, dass der Agent tut, und wie du ihn konsistent dazu bringst, das zu tun, was du willst
Wir steuern Agenten, indem wir die Verteilung verschieben, aus der es sampelt, mit anderen Worten: Prompting.
Im Jahr 2022 kam ReAct heraus und hat im Wesentlichen die Steuerbarkeit von Agenten revolutioniert. Tatsächlich können wir so weit gehen zu sagen, dass es Agenten, wie wir sie kennen, überhaupt erst möglich gemacht hat. Das Denken und Nachdenken über eine Tool-Ausgabe, bevor ein nächster Schritt unternommen wird, hält die Schleife kohärent.

Wir brauchten trotzdem intelligentere Agenten. Die Nutzung von Testzeit-Rechen-Scaling, produktiviert durch @OpenAIs O-Serie von Modellen, gab Modelllaboren die Fähigkeit, besseres Agentenverhalten einzubacken [[11]](http://localhost:5173/blog/onyx#ref-11). Das Ausgeben von mehr Tokens vor dem Aufruf eines Tools erlaubt es dem Modell, der Ausgabeverteilung zu entkommen, in der es stecken geblieben wäre, wenn es in der Länge des Reasoning-Outputs eingeschränkt gewesen wäre. Du kannst wählen, wie das Modell seine Ausgabeverteilungslandschaft durchläuft, und hast daher die Freiheit, klareres agentisches Verhalten für Aufgaben, die dir wichtig sind, zu trainieren.
Da die Kontextlänge unbegrenzt wächst, wird die Steuerung des Agenten schwierig und die Aufgabenerfüllung wird unwahrscheinlicher. Selbst mit einem Reasoning-Modell gibt es keine Garantie für eine Erholung, und der Agent stirbt genau dort. Der Agent kann sein Kontextlimit erreichen, einen vorzeitigen Abschluss erklären, in einer Schleife stecken bleiben usw.
Garantien aus einem nicht-deterministischen System ziehen
Die Lösungen dafür waren vielfältig, aber eine sticht hervor: Die Ralph-Schleife, entwickelt von @GeoffreyHuntley . [[3]](http://localhost:5173/blog/onyx#ref-3
Er führte die Idee ein, dass man die Agentenausführung begrenzen und diese Grenzen dann nutzen kann, um über die Aufgabenerfüllung nachzudenken. Dies erlaubt der Ralph-Schleife, etwas Magisches zu tun: Sie bietet etwas, auf das man sich in einem nicht-deterministischen System verlassen kann.
Ein Funke Determinismus.
Besser, einen Fehler zu garantieren und sich schrittweise in Richtung einer korrekten Lösung zu bewegen, als den Hebel des Spielautomaten noch einmal zu ziehen. Diese definierte Grenze gibt dir etwas Konkretes, über das du nachdenken kannst, und sobald du über die Grenzen von etwas nachdenken kannst, kannst du ein System daraus machen.
Gegen die Grenzen der Kontextlänge ankämpfen
Es gibt jedoch ein Problem: Ein frischer Agent verliert über mehrere Läufe hinweg die Kohärenz, aber ein einzelner Agent geht bei ausreichend Zeit der Kontext aus.
Hier kommt RLM von @lateinteraction @a1zhang ins Spiel. RLM gab uns ein Konzept, wie man auf strukturierte Weise mit langem Kontext (d.h. einem Agentenlauf) interagieren kann [[4]](http://localhost:5173/blog/onyx#ref-4). RLM wurde von CodeAct inspiriert, einem Paper aus dem Jahr 2024, das die Verwendung von Code zur Orchestrierung von Operationen demonstrierte [[5]](http://localhost:5173/blog/onyx#ref-5). Der Agent schreibt Skripte, die Operationen innerhalb einer REPL orchestrieren, um dann eine Ausgabe abzurufen. RLM funktioniert auf die gleiche Weise, mit dem zusätzlichen Vorbehalt, dass es Variablen verwendet, um Kontext zu speichern und Operationen an diesem Kontext durchzuführen. Es erlaubt zusätzlich rekursive LLM-Aufrufe in der REPL. Du verlierst vielleicht etwas von der Reaktivität, die andere Schleifen haben, gewinnst aber die Fähigkeit, programmatisch mit Kontext zu arbeiten. Das Entscheidende hier ist, dass die Skripte in der REPL flüchtig sind. Du bekommst eine Skript-Laufzeitumgebung und Kontextverwaltung, aber es gibt keine Wiederverwendbarkeit oder Komponierbarkeit. Schreibe einfach das Skript, führe es aus, und es verschwindet. In Bezug auf den Bau von Systemen ist dies streng genommen schlechter, als nur Agenten und Markdown-Dateien mit Bash-Skripten zu verketten, weil du Persistenz und begrenzte Ausführung verlierst.
Von einzelnen Schleifen zur skalierenden Orchestrierung
OpenAIs Deep Research[[6]](http://localhost:5173/blog/onyx#ref-6) war eines der frühesten Beispiele für einen deterministischen Workflow, der eine allgemeine Ausführungsform oder ein Schema mit geringer Variabilität von Lauf zu Lauf hatte. Es funktioniert, indem es eine Reihe von Abfragen plant, sie im Web ausführt, die Ergebnisse überprüft und die nächste Reihe von Abfragen plant. Jede Reihe dringt tiefer in den Problemraum ein.

Cursor trieb die Idee des Determinismus viel weiter, als @wilsonzlin ein Geschirr demonstrierte, das Agenten orchestrierte, um einen Browser zu bauen. Er baute ein maßgeschneidertes Geschirr zur Koordinierung großer Arbeitsmengen unter Verwendung paralleler Planungsagenten und Aufgabenagenten [[7]](http://localhost:5173/blog/onyx#ref-7). Was hier relevant ist, ist, dass die Beziehung zwischen jedem Teil des Geschirrs festgelegt ist. Es gibt Planer, die den aktuellen Systemzustand erkunden und Aufgaben generieren, und es gibt Ausführer, die Aufgaben übernehmen und parallel implementieren. Es gibt feste Leitplanken zwischen Agenten und feste Kanäle zur Kommunikation von Informationen. Um Koordination gut zu machen, brauchst du Garantien auf Schnittstellen.
Verwendung von Terminationsbedingungen für begrenzte Ausführung
Im Mai führte Codex die Idee eines Ziels ein, das eine Verifiziererschleife verwendet, um gegen einen gewünschten Endzustand zu hillclimben, bis eine Aufgabe abgeschlossen ist. Du kannst dir das als eine produktionsreife Version der Ralph-Schleife vorstellen, die in Codex eingebaut ist. Es erlaubt dir, ein Ziel anzugeben, und hat eine automatisierte Schleife, die ausführt und überprüft, eingebaut.

Karpathys Autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9) ist ähnlich wie Codex' /goal und der Ralph-Schleife. Es kombiniert die überprüfbare Terminationsbedingung von goal mit der Ausführungsbegrenzung einer Ralph-Schleife über Iterationen, was es ihm erlaubt, kontinuierlich auf ein Ziel zuzusteuern. Es macht Fortschritte, indem es den Ideenraum durchsucht und sich iterativ im Laufe der Zeit verbessert.

Bis zu diesem Punkt sind alle Lösungen, die die Orchestrierung außerhalb des Agenten externalisieren, in ihrer Ausführungsgraphenform festgelegt. Sie laufen mit einem handgeschriebenen Muster und haben eine Art Schema für die erlaubten Formen, in denen sie operieren können. Sie passen sich nicht pro Aufgabe an oder sie haben keine starken Garantien über die Ausführungsgraphenform überhaupt.
Orchestrierung flexibel machen
Im März dieses Jahres haben wir Slate vorgestellt, den ersten Coding-Agenten, der Code für die Live-Subagenten-Orchestrierung im Stil von RLM verwendet. Es ist immer noch der einzige gut genutzte Coding-Agent, der Code für die Live-Agenten-Orchestrierung verwendet. In Slate können Threads in Echtzeit erzeugt, pausiert, fortgesetzt und gesteuert werden. Der Hauptagent versteht zutiefst, wie alle laufenden Subagenten orchestriert werden, so dass du es nicht tun musst. Ähnlich wie bei RLM standen wir jedoch immer noch vor der Herausforderung, Zustände über Subagenten hinweg zu teilen und flüchtige Skripte zu verwenden, was nicht etwas ist, auf das du mit einem Bash-Skript und einer Markdown-Datei stoßen würdest.
Selbst dann, wenn das Modell die Orchestrierung übernimmt, wie steuerst du es? Sagst du ihm, es solle seinen Orchestrierungscode auf eine bestimmte Weise schreiben? Was tust du?
Unsere anfängliche Lösung (als Patch, bevor wir die Onyx-Laufzeitumgebung veröffentlichten) hieß Orchestrierungsfähigkeiten [[13]](http://localhost:5173/blog/onyx#ref-13). Die Idee war einfach: Erlaube dem Benutzer, eine Fähigkeit bereitzustellen, um zu steuern, wie der Agent seine Orchestrierung angeht. Das war's. Es funktionierte einigermaßen, hatte aber viele Probleme.
Ein Skill ist nämlich kein verbindlicher Verhaltensvertrag. Aus Text kannst du keine Garantie ziehen.
Das bedeutet, dass der Orchestrator dem gewünschten Ausführungsmuster nicht folgen musste, weil es keine wirkliche Möglichkeit gab, es durchzusetzen. Einer der größten Vorteile der Onyx-Laufzeitumgebung ist, dass wir dieses Problem gelöst haben.
Keines der genannten Systeme hat verbindliche Verhaltensverträge.
Nun, was wäre, wenn der Agent seinen Orchestrierungscode pro Aufgabe in ein Skript schreiben könnte, so dass der Ausführungsgraph festgelegt ist? Das ist es, was Claude Dynamic Workflows ist.[[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) Auf die gleiche Weise wie RLM und Slate, indem Code geschrieben wird, um Subagenten zu orchestrieren, erlaubt Dynamic Workflows Claude, Workflow-Formen zu schreiben und zu speichern. Dies kombiniert sich mit /loop, um über bestimmte Muster loopen zu können. Es bietet einen deklarativen Vertrag für das Verhalten einer Gruppe von Agenten. Es ist immer noch nicht dasselbe wie Software zu schreiben, da ihm Dinge wie funktionale Komposition fehlen, aber du erhältst Persistenz und eine starke Garantie darüber, wie die Aufgabe ausgeführt wird. Es sind dynamisch geschriebene Workflow-Skripte für eine bestimmte Aufgabe ad hoc.[[12]](http://localhost:5173/blog/onyx#ref-12) Und da sie auf der Festplatte gespeichert werden, haben sie einen zusätzlichen Vorteil: sie können erneut ausgeführt und mit Orchestrierungskleber wie /loop umhüllt werden.

Wenn du es bemerkst: Alle oben genannten Lösungen streben nach dem Gleichen: einer deterministischen Möglichkeit zu steuern, wie Agenten im Laufe der Zeit ausgeführt werden.
Dies ist eine Geschichte, die wir bereits in der Softwareentwicklung als Fachgebiet ablaufen sahen. Wir begannen damit, unterschiedliche Systeme zusammenzukleben und Jobs zu scripten, und dann wurden unsere Sprachen flexibler und leistungsfähiger. Wir gewannen mehr und mehr Hebelwirkung über den Entwicklungsprozess mit stärkeren Ökosystemen, die es uns erlaubten, zuverlässigere Systeme auf höheren Abstraktionsebenen zu bauen.
Im Moment befinden sich Agenten auf derselben Flugbahn, und heute veröffentlichen wir den nächsten Schritt entlang dieser Flugbahn, der es dir erlaubt, die Systeme zu entwickeln, die deine Agenten ausführen. Programmiersprachen verwenden oft Interpreter oder VMs, um Ressourcen automatisch zu planen. Das ist es, was dir als Ingenieur, der die Sprache verwendet, Hebelwirkung verschafft.
Wenn eine VM für die Agenten-Orchestrierung sinnvoll sein soll, brauchst du ein paar Dinge:
- Persistentes Zustandsmanagement: Wir sollten in der Lage sein, Zustände zu definieren, sie beim Namen zu referenzieren, zu persistieren und programmatisch zu manipulieren.
- Typgarantien. Wir sollten definierte Eingabe- und Ausgabeformen respektieren und ihnen folgen und uns auf sie verlassen können.
- Kontrollfluss-Primitive, vorzugsweise bekannte, die ein LLM verstehen würde.
- Klare Struktur für die Fehlerbehandlung (z.B. try-catch).
- Ressourcenmanagement: Definierte Kontrollen über Ressourcen wie Agentenparallelität, Kosten, welche Modelle laufen usw.
- Ausführungsisolierung: Ein bestimmter laufender Agent oder ein bestimmtes Programm sollte von einem anderen isoliert sein, es sei denn, der Zustand wird explizit geteilt.
- Lebenszykluskontrolle: Wie ein Agentenprogramm aussieht und Semantiken zum Ausführen, Abbrechen und Steuern. Ohne dies hast du keinen klaren Weg zur Bereinigung und kannst das Lebenszyklusmanagement nicht kontrollieren.
- Komponierbarkeit: Programme sollten ineinander komponierbar sein und mit definierten Eingabe- und Ausgabetypen aufrufbar sein.
- Sichtbarkeit: Wir sollten wissen können, was wann gelaufen ist, und in der Lage sein, einen Ausführungsfehler in der Quelle zurückzuverfolgen.
- Haltbarkeit: Wir sollten ein klares Modell dafür haben, wie wir uns von Abstürzen erholen und fortsetzen können.
Jedes einzelne dieser Probleme wurde bereits vor Jahrzehnten von Programmiersprachen gelöst. Die Agenten-Orchestrierung stößt nur alle zum ersten Mal wieder darauf.
Um wirklich Software dafür schreiben zu können, muss ein "program.ts"-Programm in einer Laufzeitumgebung verfasst werden, die all das oben Genannte unterstützt, damit wir darüber nachdenken können, was passiert, wenn ein Programm nicht funktioniert, und um den Fehler herum entwickeln können.
Deshalb haben wir Onyx gebaut. Es ist eine Agenten-Orchestrierungs-VM, die genau dafür entwickelt wurde, sowohl persistente komponierbare Programme als auch eine interpretierte Skripting-Ebene zu unterstützen. So funktioniert es und was eine "program.ts"-kompatible Laufzeitumgebung unterstützen muss.
Entwicklung der Laufzeitumgebung
Wenn wir eine Sprache und eine Laufzeitumgebung für diese Sprache entwerfen, müssen wir über die Einschränkungen nachdenken, über die wir nachdenken wollen, und darüber, was uns wichtig ist, dass es leicht ausdrückbar ist. Dann können wir die resultierenden Semantiken in zwei Kategorien unterteilen: statische Semantiken und Laufzeitsemantiken.
Statische Semantiken sind all die Dinge, die über ein Programm abgeleitet werden können, indem man es sich nur ansieht. Die Dinge, die ein Compiler oder Typprüfer über ein bestimmtes Programm weiß.
Laufzeitsemantiken definieren, was der Code tatsächlich bedeutet und wie das Programm tatsächlich läuft. Dies umfasst die zugrunde liegende Ressourcenzuweisung und Planungsmechanik.
Unser Ziel mit einer Laufzeitumgebung für Agenten ist es, den Orchestrierungskontrollfluss in Code zu verwandeln, und wir wollen den Ausführungszustand persistent und typisiert machen, damit wir ihn zuverlässig zur Steuerung der Orchestrierung verwenden können.
Ein paar VM-Anforderungen
Es gibt 3 VM-spezifische Dinge, die uns über die normale TypeScript-Ausführung hinaus wichtig sind.
- Als Agenten-Orchestrierungslaufzeit muss sie in der Lage sein, Agenten zu orchestrieren. Das bedeutet, sie zu erstellen, ihre Lebenszyklen zu verfolgen usw. Wir wollen, dass die Laufzeit sie auf blockierende oder nicht-blockierende Weise ausführen und korrekt planen kann.
- Wir wollen Kontrolle über die Ausgabeformen von Agenten und wollen eine strenge Durchsetzung des Ausgabevertrags.
- Wir wollen zur Laufzeit Kontrolle über externe Ressourcen wie Modelle und Kosten haben.
Ausführen von Agenten und Programmen
Um einen Agenten auszuführen, haben wir zwei grundlegende Verben ausgewählt: run und spawn. Run führt einen blockierenden Agenten im Vordergrund aus. Spawn führt einen Agenten im Hintergrund aus. Dies entspricht dem allgemeinen Verständnis von spawn, wie posix_spawn, und macht es einem Modell leicht, unsere neuen Verben zu verstehen, da sie konzeptionell in den Trainingsdaten enthalten sind. Spawn und run erlauben es dir, Agenten und Programme, die von der Festplatte gelesen werden, direkt aufzurufen und geben genügend Informationen für ein Ausführungshandle zurück.
Run unterstützt auch ein paar Dinge. Es unterstützt direkt erzwungene Ausgabetypen durch zod @colinhacks und es unterstützt direkte Modellüberschreibungen, was es einfach macht, Programme zu schreiben und auszuführen, bei denen das Aufteilen auf mehrere verschiedene Modelle für verschiedene Lösungen oder verschiedene Schritte einer Aufgabe sinnvoll ist.
1function run<S extends z.ZodType>(2 name: string,3 options: ...4): Promise<z.infer<S>>
Run erlaubt es dir, Subagenten direkt inline zu verketten.
1// einfacher Agentenlauf2const out = await run({ type: "read", prompt: () => "Antworte mit: ok" })3// benannter Lauf (string = untergeordnete WorkflowId)4const review = await run("reviewer", {5 type: "general",6 prompt: () => "Überprüfe den Diff",7})8// strukturierte Ausgabe (typisiertes Ergebnis)9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })10const v = await run({11 type: "general",12 prompt: () => "Bewerte das Risiko",13 output: Verdict,14})
Spawn ist ähnlich wie run, erzeugt aber einen Agenten im Hintergrund. Erzeugte Subagenten werden nicht abgewartet und der Kontrollfluss geht einfach weiter. Spawn ist sehr nützlich, um mehrere nicht blockierende Ausführungsagenten zu starten.
1// Hintergrundagent2const h = await spawn("worker", { type: "general", prompt: "Lange Aufgabe" })
Interaktion mit laufenden Agenten
Wir wollen zwei Arten von Operationen an laufenden Agenten durchführen können: Steuern und Stoppen.
Eine Steuernachricht ist eine Nachricht, die an den Agenten gesendet wird, die das LLM während des Laufs erhält, um es in eine Richtung zu lenken. Dies ist nützlich, um den Aufgabenkontext des Agenten zu aktualisieren, ohne den Worker herunterfahren zu müssen.
Abbruch ist ebenfalls wichtig, wir wollen in der Lage sein, einen Subagenten aktiv zu beenden, wenn er nicht laufen sollte.
Die Fähigkeit, diese Operationen sowohl von der Live-REPL als auch von einem vorab verfassten Programm aus auszuführen, gibt Slate seine Fähigkeit, alles in Echtzeit zu orchestrieren. Es kann die Form der Orchestrierung zur Laufzeit dynamisch definieren oder echte Software zur Orchestrierung verfassen und daran iterieren.
Slate ist in der Lage, Programme in \.program.ts-Dateien zu schreiben. Eine Programmdatei hat ein paar Dinge: ihren Namen (so weiß Slate, was es ist), eine JSDoc-Beschreibung und dann den eigentlichen Programmtext*. Eine Programmerklärung sieht so aus:
1program(async (ctx) => {2 // günstiges Modell für die Suche – es muss nur Dateien finden3 const findings = await run("search", {4 type: "read",5 prompt: "Finde alle authentifizierungsbezogenen Dateien",6 model: "codex/gpt-4.1-mini", // verwendet deinen integrierten Codex-Schlüssel7 })8})
Programme folgen dem gleichen asynchronen Ausführungsmodell, das es uns erlaubt, ein Programm sowohl im Vordergrund als auch im Hintergrund auszuführen und mit ihm zu interagieren, während es läuft.
1// Hintergrundagent2const h = await spawn("worker", { type: "general", prompt: "Lange Aufgabe" })3await h.notify("konzentriere dich zuerst auf den Parser") // Steuernachricht an den laufenden Agenten4const result = await h.result() // warte später auf den Abschluss56// aufteilen, dann sammeln7const a = await spawn({ prompt: "Aufgabe A" })8const b = await spawn({ prompt: "Aufgabe B" })9const [ra, rb] = [await a.result(), await b.result()]1011// ein Programm im Hintergrund12import Audit from "deep-audit"13const ah = await spawn(Audit, { input: { pr: 42 } })14const auditResult = await ah.result()
Strukturierte Ausgabe und Zustand
Dies ist eine grundlegende Einschränkung jedes anderen Systems bis heute. Der Zustand ist in allen anderen Systemen schlecht externalisiert und nicht sicher isoliert. Wenn es eine Datei auf dem System ist, kannst du keine Korruption garantieren. Wenn du das kannst, kannst du immer noch keine Parsebarkeit garantieren. Du kannst keine Zustandsänderungen abonnieren, um Operationen zu steuern, und du kannst keine Typeinhaltung garantieren.
Erinnerst du dich, wie wir persistenten Zustand wollten, der auch strukturiert und referenzierbar ist?
Der Zustand in Onyx ist anders. Zustandsnamensräume werden deklariert, direkt benannt und über die Zeit persistiert. Das bedeutet, dass ein Zustandsspeicher immer wieder verwendet werden kann, was es dir erlaubt, langlebige Agentensysteme mit echten Daten zu bauen.
Sowohl Agenten als auch Code lesen den Zustand, und der Determinismus, den wir von einer Laufzeitumgebung wollten, ergibt sich daraus. Agenten lesen den Zustand über ein dediziertes Tool, das es ihnen erlaubt, immer auf sichere, strukturierte Weise mit ihm zu interagieren. Agenten und Programme sind beide Konsumenten, die gesteuert werden können, um den Zustand zu ändern, was es der Laufzeit erlaubt, sich auf das Zustandsobjekt zu verlassen, um die Orchestrierung zu steuern.
Zustand und Schemaeinhaltung steuern den Abschluss von Subagenten. Aus diesem Grund bietet der Zustand eine einheitliche Oberfläche zur Steuerung des gesamten Programms.
Zustandsobjekte können auch als Laufzeitvariablen an untergeordnete Sitzungen weitergegeben werden, die mit dem Hauptagenten geteilt werden. Dieser Zugriff per Referenz in der gesamten Agentenhierarchie (was ein Novum ist) ermöglicht die Kommunikation zwischen Agenten über einen gemeinsamen Zustandskanal.

Langlaufende Schleifen
Einige Programme müssen eher wie laufende Systeme funktionieren. Nimm zum Beispiel OpenClaw. Du kannst OpenClaw tatsächlich als Programm darstellen, wenn du die richtigen Primitive hast. Dafür verwenden wir zwei Primitive: sleep und checkpoint.
Sleep tut, was du erwarten würdest, es schläft.
Nun, hier ist die Sache: Nehmen wir an, du möchtest eine langlaufende Aufgabenverwaltung im Hintergrund. Ein vordefinierter Ausführungsgraph könnte stecken bleiben oder kaputtgehen, und daher ist es wichtig, dass der Hauptagent den Status des Programms kennt.
Um dies zu unterstützen, führen wir das Checkpoint-Primitiv ein.
Ein Checkpoint kann alles sein, aber der Grund, warum es Checkpoint heißt, ist, dass es den Hauptagenten mit einem Objekt fester Form benachrichtigt. Dies erlaubt dem Hauptagenten, Dinge wie den Aufgabenfortschritt zu verfolgen und über Änderungen des Programmzustands direkt benachrichtigt zu werden. Im Gegenzug kann der Hauptagent dann ein laufendes Programm effektiver verwalten.
Onyx unterstützt es, einen Agenten wie OpenClaw loopen zu lassen, d.h. einen persistenten Agenten mit einem Heartbeat.
Das ist wirklich cool – du kannst die Primitive zu einem völlig anderen Agententyp zusammensetzen, indem du einfach eine While-Schleife, einen Sleep und einen Checkpoint verwendest.
Openclaw kann einfach als Programmdatei dargestellt werden!
1// Ein Programm für eine lang laufende Auto-Research-Schleife2for (let i = 0; i < maxExperiments; i++) {3 const idea = await run("propose", { ... })4 const result = await run("train", { ... })5 checkpoint({ message: `experiment ${i}`, data: { idea, result } })6 await sleep(30_000) // Abkühlphase zwischen Experimenten7}89// Ein Programm für einen persistenten Agenten im Openclaw-Stil10while(true) {11 const status = await run("status_check", { ...insert cheap model here... })if(status.pending_tasks) {checkpoint({ tasks: status.pending_tasks }) // den wichtigen Status zurückgeben und den Hauptagenten aufwecken}12 await sleep(30_000) // Abkühlphase zwischen Experimenten13}
Komposition
Mit Onyx kann Slate eine *.program.ts für dich schreiben. Diese bleibt bestehen und kann (und sollte) wie normaler Code behandelt werden. Sie hat Typen, die standardmäßig enthalten sind, läuft in einer Runtime ohne globale Runtime-Variablen und ist einfach TypeScript, sodass ihr Kompositionsmodell einfach darin besteht, ein anderes Programm zu importieren und aufzurufen.
Da es sich um TypeScript handelt, bekommst du Dinge wie Parallelität (Promise.all) und Schleifen kostenlos dazu.
So importierst du ein Programm und verwendest es in einem anderen:
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", ... audit output) // dies würde laufen und die Ausgabe des Audit-Programms korrigieren.4})
Fehlersemantik
Fehler werden in der idealen VM laut geworfen. Dies sollte bei Laufzeit-Syntaxproblemen, Agentenfehlern, Abstürzen usw. geschehen.
Insbesondere definieren wir Orchestrierungsfehler wie folgt:
- Ein Agent ist bei einer Aufgabe blockiert
- Ein Agent konnte eine Aufgabe nicht abschließen
- Ein Agent hat die Schritte oder das Budget für eine Aufgabe aufgebraucht
- Ein Programm hat das Budget für einen Durchlauf aufgebraucht
- Das Orchestrierungsmodell konnte keinen syntaktisch korrekten Code schreiben
- Eine illegale Zustandsänderung wurde vorgenommen
All diese spezifischen Fehlerfälle definieren die Laufzeitsemantik. Sie besagen: „Du kannst erwarten, dass diese Runtime einen Fehler wirft, weil wir einen fehlgeschlagenen Agenten genauso behandeln wie einen Fehler im Code." Das mag zunächst lästig erscheinen, aber dieser laute Fehlermechanismus gibt dir etwas zurück: eine explizite Möglichkeit, sich auf Fehler vorzubereiten und um sie herum zu programmieren. In Wirklichkeit gibt es dir also mehr Kontrolle, nicht weniger.
1// Fehler werden mit try/catch behandelt – genau wie in jedem TypeScript-Programm2program(async (ctx) => {3 try {4 const result = await run("risky-refactor", {5 type: "general",6 prompt: "Refaktoriere das Auth-Modul",7 model: "claude-sonnet",8 maxSteps: 20,9 })10 } catch (err) {11 // Der Agent ist fehlgeschlagen – aber wir wissen genau, warum.12 // Der Trace enthält jeden Tool-Aufruf, jede Modellanfrage,13 // jeden Zustandswrite, der hierher geführt hat.1415 // Wiederholung mit einem anderen Modell16 const result = await run("risky-refactor-retry", {17 type: "general",18 prompt: `Der vorherige Versuch ist fehlgeschlagen: ${err.message}. Versuche einen anderen Ansatz.`,19 model: "claude-opus",20 maxSteps: 30,21 })22 }23})
Modellauswahl, Budgetkontrolle und BYOK
Die integrierte Modellauswahl ermöglicht eine noch präzisere Steuerung. Die /models-Fähigkeit gibt Slate vollen Zugriff auf die Liste der verfügbaren Modelle, sodass Slate Programme mit mehreren verschiedenen Modellen für unterschiedliche Aufgaben erstellen kann. Soll Fable der Planer sein, aber GLM 5.2 die Implementierung in einer deterministischen Umgebung übernehmen? Kein Problem. Möchtest du eine Frage auf Gemini, GPT 5.5 und DeepSeek verteilen? Auch das funktioniert.
Darüber hinaus unterstützt die Runtime zwei Arten von Konfigurationsüberschreibungen für Programme:
- Die standardmäßigen globalen Modelle, die für die Agentenausführung verwendet werden
- Das Budget für die Ausführung eines Programms
Du kannst direkt ein Ausführungsbudget festlegen, um die Ausgaben für eine bestimmte Schleife zu begrenzen.
Zusätzlich unterstützt die Runtime die Verwendung deiner bestehenden OpenAI- und Github Copilot-Abonnements.
1program(async (ctx) => {2 // Günstiges Modell für die Suche – es muss nur Dateien finden3 const findings = await run("search", {4 type: "read",5 prompt: "Finde alle authentifizierungsbezogenen Dateien",6 model: "codex/gpt-4.1-mini", // verwendet deinen integrierten Codex-Schlüssel7 })89 // Reasoning-Modell für den schwierigen Teil – es muss denken10 const plan = await run("architect", {11 type: "general",12 prompt: `Entwerfe eine Lösung basierend auf: ${findings.output}`,13 model: "openai/o3", // Verwendet API-Guthaben14 output: z.object({15 approach: z.string(),16 files: z.array(z.string()),17 risk: z.enum(["low", "medium", "high"]),18 }),19 })2021 // Mittelklasse-Modell für die Implementierung – es muss nur bearbeiten22 const handles = await Promise.all(23 plan.files.map(f => spawn("fix-" + f, {24 type: "general",25 prompt: `Wende diese Lösung auf ${f} an: ${plan.approach}`,26 model: "anthropic/claude-sonnet-5",27 maxSteps: 15,28 }))29 )30 await Promise.all(handles.map(h => h.result()))31})
Definition der Autorenoberfläche
Bei der Gestaltung der Autorenoberfläche für Programme gab es zwei Hauptfaktoren: wie einfach es für einen Agenten ist, sie zu verstehen, und wie einfach es für einen Menschen ist, sie zu lesen. Wir haben uns für relativ einfache Verben entschieden, die sich wie Englisch lesen, und haben bewusst entschieden, die Orchestrierung prozedural und nicht deklarativ zu modellieren.
Die Auswahl von TypeScript als Sprache war ebenfalls wichtig. Es gibt so viel prozeduralen TypeScript-Code im Umlauf, dass ein Modell die TypeScript-Semantik implizit versteht, selbst ohne Nachbearbeitung.

Engineering-Komponenten unserer Softwarefabrik
Die nächste zu beantwortende Frage ist: Was bringt dir das alles?
Es gibt dir die Möglichkeit, echte Software für deine Agentenorchestrierung zu schreiben. Du kannst jetzt deine eigene Agentenorchestrierung von Anfang bis Ende entwickeln.
Du kannst die Fabrik entwickeln.
Zum Beispiel kannst du ein Programm erstellen, das GitHub in einer Schleife überwacht, und ein separates Programm, das einen Implementierungsagenten mit einem QA-Agenten zur Überprüfung ausführt. Beides sind einzeln nützliche Muster, die dir in der Praxis begegnen könnten. Dann kannst du sie zusammensetzen, um ein System zu erstellen, das auf Kommentare zu einem PR hört, einen Implementierer startet, um diese Kommentare zu adressieren, und dann einen QA-Agenten startet, um sicherzustellen, dass die Lösung gültig ist.
Du kannst dieses Programm dann mit einer Aufgabenwarteschlange verbinden, um Arbeiten an deiner Codebasis zu delegieren und zu überwachen und automatisch auf PR-Kommentare zu reagieren.
Und das alles kannst du mit schnellen Open-Weight-Modellen tun. Da es sich nur um Code handelt, brauchst du kein leistungsstarkes LLM, um die Orchestrierung nach der ersten Erstellung durchzudenken.
Kommen wir nun zum unterhaltsamen Teil: Zeit, einige der Programme zu teilen, die wir für massive Produktivitätssteigerungen verwendet haben.
Tiefgehende Codebasis-Recherche
Wir verwenden dieses Programm, um Aufgaben abzugrenzen. Es führt eine tiefgehende Recherche zum Zustand unseres Monorepos durch und erstellt ein Recherchepaket, auf das ein Implementierer zurückgreifen kann. Wir verwenden es ständig. Es klingt teuer, ist es aber nicht. Du kannst dieses Programm in Slate mit DeepSeek V4 Flash ausführen, und der Rechercheprozess ist gründlich, aber spottbillig.

Ziel-Review-PR
Dieses Programm verwenden wir, um eine Aufgabe zu implementieren, sobald die Recherche abgeschlossen ist. Glücklicherweise ist die meiste Aufgabenunschärfe bis zu dem Zeitpunkt, an dem die Recherche das Zielprogramm erreicht, bereits ausgeräumt, was die Ausführung der Aufgabe noch schneller macht. Die Vorverlagerung der Recherche mit einem leichten OSS-Modell macht es uns leicht, ein teures Modell wie Opus für das zu verwenden, worauf es ankommt: wirklich guten Code zu schreiben und den Zustand des Systems zu überprüfen. Du könntest das Programm sogar so modifizieren, dass GPT 5.5 die Arbeit von Opus 4.8 adversariell überprüft.

Autoresearch als Programm
Autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9) war ursprünglich vollständig LLM-gesteuert. Richte einen Agenten auf die program.md-Eingabeaufforderung aus, und er entscheidet, was er versuchen und wie er vorankommen soll.
Es überrascht nicht, dass Autoresearch eigentlich nur ein Programm ist.
Agentenprogramme ermöglichen es dir, dies umzukehren und den Kontrollfluss in die Runtime zu verlegen. Das Programm besitzt den Kontrollfluss, während die Agenten die nebenwirkungsbehaftete Arbeit erledigen (Code bearbeiten, Git ausführen, SSH zum entfernten GPU-Server herstellen, trainieren). Für das Autoresearch-Programm ist die Entscheidung zum Behalten/Zurücksetzen deterministischer Code:
1kept = status === "ok" && valBpb != null && valBpb < best
In unserem Fall führt das Programm einen Setup-Agenten aus, um ein frisches Repository vorzubereiten und zu überprüfen, ob die entfernte A100 erreichbar ist. Wenn das Setup fehlschlägt, wird frühzeitig mit einem sauberen Exit basierend auf einem typisierten Wert zurückgekehrt. Andernfalls wird die Experimentierschleife betreten.
Jedes Experiment erhält einen neuen Agenten. Der Agent erhält die aktuelle beste Konfiguration und den Verlauf früherer Ideen und Ergebnisse, damit er sich nicht wiederholt und auf dem aufbauen kann, was behalten wurde. Er schlägt eine Änderung vor, bearbeitet train.py, committed, synchronisiert per rsync auf die entfernte Maschine, trainiert und klassifiziert das Ergebnis.
Der Agent und das Programm teilen sich den Zustand. Der Agent schreibt Daten in den Zustand, und das Programm wertet den Zustand für den Kontrollfluss aus. Basierend auf dem Ergebnis aktualisiert ein Aufzeichnungsagent results.tsv und setzt optional den Lauf zurück, wenn das Programm entschieden hat, das Experiment zu verwerfen. Dadurch zeigt git HEAD immer auf den aktuell besten Branch des Experimentbaums.
Es gibt zwei Kernunterschiede, die es zu beachten gilt: 1) Dies läuft in einem Programm, sodass wir können einen neuen Agenten pro Experiment starten, und 2) wir können basierend auf dem Live-Programmzustand entscheiden, welche Aufgabe der Agent erledigen soll.


Und so sieht es im Code aus:
1// ---------- Programm ----------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: `setup fehlgeschlagen: ${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 // Agent-Fehler/Blockade – als Absturz behandeln, Repo auf Besten zurücksetzen, fortfahren.32 exp = {33 description: `experiment ${i} agent error`,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: `experiment-${i}`,71 message: `exp ${i}/${total}: ${exp.status}${kept ? " BEHALTEN" : ""} val_bpb=${exp.valBpb ?? "n/a"} (best=${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})
Zukünftige Arbeiten
Die einzige verbleibende VM-Anforderung, die wir noch nicht definiert haben, ist das Haltbarkeitsmodell für Programme. Es ist unklar, welches das richtige Modell für die Wiederaufnahme und Behandlung des Lebenszyklus eines Programms ist und welche Kontrollebene der Runtime ausgesetzt werden sollte.
Darüber hinaus gibt es so viele aufregende Dinge, die wir hinzufügen werden, um verschiedene Arbeitslasten und Aufgabenformen zu unterstützen, damit wir echte Software schreiben können, um Agenten besser zu orchestrieren. Wir sind überzeugt, dass viele der Muster daraus entstehen werden, dass Menschen Programme auf kreative Weise selbst nutzen.
Wir können es kaum erwarten zu sehen, was ihr baut.
- RL Team
Referenzen
- Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
- Geoffrey Huntley, "The Ralph Loop"
- Geoffrey Huntley, "everything is a ralph loop," Januar 2026
- Zhang, Kraska, Khattab, "Recursive Language Models," Dezember 2025
- Wang et al., "Executable Code Actions Elicit Better LLM Agents," ICML 2024
- OpenAI, "Introducing Deep Research," Februar 2025
- Cursor, "Scaling Agents," Januar 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"





