Progettare un runtime programmabile per l'orchestrazione di agenti

@realmcore_
INGLESE22 ore fa · 03 lug 2026
283K
260
45
10
456

TL;DR

Onyx introduce un runtime programmabile per agenti AI, consentendo agli sviluppatori di creare sistemi deterministici utilizzando TypeScript, stato persistente e gestione strutturata degli errori.

Questo articolo presenta Onyx, la nostra VM per l'orchestrazione programmabile di agenti. E, per estensione, un runtime che trasforma l'orchestrazione in ingegneria del software. Alla fine di questo articolo, capirai i vincoli e le decisioni di progettazione alla base della creazione della VM, oltre a come creare i tuoi programmi e progettare i tuoi sistemi di agenti.

Introduzione

Gli agenti sono intrinsecamente non deterministici. Questo è il punto centrale. Se volessi il determinismo, scriveresti software.

Ma a un certo punto, tutti coloro che usano agenti hanno voluto collettivamente spingersi oltre. Abbiamo imparato che suddividere l'esecuzione in passaggi strutturati aiuta le prestazioni: Pianifica, Implementa, Revisiona, QA, ecc. Poi abbiamo apparentemente concordato di scrivere script, strumenti e competenze per guidare ogni agente, condividere il contesto tra di loro e metterli in sicurezza. Successivamente, uniamo questi script incanalando testo tra gli agenti e, poiché ci limitiamo a passare testo, funziona più o meno.

Se avessi dedicato abbastanza tempo al problema e fossi stato particolarmente astuto, avresti capito come ottenere garanzie dal tuo sistema in modo da poter avere un'esecuzione condizionale basata su un dato stato. E probabilmente memorizzeresti quello stato in un file di markup analizzabile o in un insieme di file di markup per guidare i tuoi script bash. Potresti anche aver creato una CLI personalizzata per i tuoi agenti.

Come ingegneri, questo ci è familiare: usiamo script mentre facciamo ingegneria del software. Tuttavia, il software moderno non viene costruito concatenando script bash e strumenti CLI. Invece, abbiamo linguaggi di programmazione, runtime e toolchain che ci aiutano a progettare i nostri sistemi. Scriviamo software con linguaggi di programmazione perché sono dotati di una libreria standard, semantica chiara e un modello di esecuzione su cui possiamo fare affidamento. Hanno ecosistemi ricchi con toolchain per tutte le nostre esigenze.

Le garanzie che ci danno sui nostri sistemi ci permettono di ragionare a livelli di astrazione più elevati.

Ma non esiste un equivalente per progettare sistemi di agenti. Per costruire sistemi, l'orchestrazione degli agenti deve essere programmabile esattamente come il software moderno.

Oggi presentiamo le specifiche per PROGRAMMI (*.program.ts), e Onyx, la nostra VM costruita per l'orchestrazione deterministica di agenti. Questo articolo esplora una storia dell'orchestrazione degli agenti, la semantica statica e runtime di una VM in grado di eseguire un programma, e le sue implicazioni per la direzione del settore.

Sembra costoso, ma in realtà non lo è. Lo spiego più avanti nell'articolo.

Per i curiosi, ecco come appare Autoresearch di Andrej Karpathy come programma:

akira - inline image

Problemi Irrisolti nell'Orchestrazione degli Agenti

Per capire cosa dovrebbe includere un runtime per l'orchestrazione degli agenti, dobbiamo comprendere i limiti degli agenti.

Un agente LLM può essere pensato come un generatore di flussi JSON, alimentato in un parser, che poi invia chiamate a strumenti in un ambiente in un ciclo.

Ogni chiamata a strumento ha la stessa forma di schema esterno, ma il contenuto di questo flusso di output non è deterministico.

La combinazione di determinismo e non-determinismo è ciò che ha reso gli agenti così preziosi. Sono abbastanza flessibili da concatenare sequenze di azioni in modi unici, ma abbastanza deterministici da interagire con un computer tramite chiamate a strumenti.

La componibilità è quasi gratuita se sei disposto a rinunciare al requisito che il contenuto di quel flusso sia tipizzato. I modelli sono abbastanza bravi da incanalare testo dentro e fuori dai binari che forniamo loro: prompt, messaggi e chiamate a strumenti.

Questo espone un'interfaccia molto componibile: il testo

Il testo è un'interfaccia universale. Tutto su un computer può essere serializzato in testo, anche se è solo codice macchina. Se puoi avere un LLM che riceve e produce testo attraverso questa interfaccia universale, ottieni componibilità sui flussi di testo.

Ciò significa che l'affidabilità del comportamento del tuo agente è direttamente correlata alla coerenza dell'output del modello. Un'elevata variabilità dell'output significa un comportamento più erratico dell'agente.

Una volta che hai un'interfaccia per comporre i pezzi, il prossimo vincolo di cui ti preoccupi è la guidabilità:

cosa vuoi che l'agente faccia e come ottieni in modo coerente che faccia ciò che vuoi

Guidiamo gli agenti spostando la distribuzione da cui campionano, in altre parole, prompting.

Nel 2022, ReAct è uscito e ha essenzialmente aperto la strada alla guidabilità degli agenti. In effetti, possiamo spingerci fino a dire che ha reso gli agenti come li conosciamo una realtà. Pensare e ragionare su un output di uno strumento prima di compiere un passo successivo è ciò che mantiene il ciclo coerente.

akira - inline image

Avevamo ancora bisogno che gli agenti fossero più intelligenti. L'uso del test time compute scaling, reso produttivo dai modelli della serie O di @OpenAI, ha dato ai laboratori di modelli la possibilità di incorporare un miglior comportamento degli agenti [[11]](http://localhost:5173/blog/onyx#ref-11). Produrre più token prima di chiamare uno strumento permette al modello di sfuggire alla distribuzione di output in cui sarebbe rimasto bloccato se fosse stato limitato nella lunghezza del ragionamento di output. Puoi scegliere di addestrare come il modello attraversa il suo panorama di distribuzione di output e, quindi, avere la libertà di addestrare un comportamento agentico più chiaro sui compiti che ti interessano.

Man mano che la lunghezza del contesto cresce senza limiti, guidare l'agente diventa difficile e il completamento del compito diventa meno probabile. Anche con un modello di ragionamento, non c'è garanzia di recupero e l'agente muore lì. L'agente può raggiungere il suo limite di contesto, dichiarare un completamento anticipato, rimanere bloccato in un ciclo, ecc.

Estrarre Garanzie da un Sistema Non-Deterministico

Le soluzioni a questo problema sono state varie, ma una si distingue: Il Ralph Loop, creato da @GeoffreyHuntley. [[3]](http://localhost:5173/blog/onyx#ref-3

Ha introdotto l'idea che potresti limitare l'esecuzione dell'agente e poi usare quei limiti per ragionare sul completamento del compito. Questo permette al Ralph Loop di fare qualcosa di magico: fornisce qualcosa su cui fare affidamento in un sistema non deterministico.

Una scintilla di determinismo.

Meglio garantire il fallimento e progredire gradualmente verso qualcosa di corretto, piuttosto che tirare la leva della slot machine un'altra volta. Questo confine definito ti dà qualcosa di concreto su cui ragionare e, una volta che puoi ragionare sui confini di qualcosa, puoi creare un sistema.

Combattere i Limiti della Lunghezza del Contesto

C'è un problema, però: un agente nuovo perde coerenza tra le esecuzioni, ma un singolo agente esaurisce il contesto con abbastanza tempo.

Entra in gioco RLM di @lateinteraction @a1zhang. RLM ci ha dato un concetto su come interagire con un contesto lungo (cioè un'esecuzione di un agente) in modo strutturato [[4]](http://localhost:5173/blog/onyx#ref-4). RLM si è ispirato a CodeAct, un articolo del 2024 che dimostrava l'uso del codice per orchestrare operazioni [[5]](http://localhost:5173/blog/onyx#ref-5). L'agente scrive script che orchestrano operazioni all'interno di un REPL per poi recuperare un output. RLM opera allo stesso modo con l'ulteriore avvertenza che usa variabili per memorizzare il contesto ed eseguire operazioni su quel contesto. Inoltre, permette chiamate LLM ricorsive nel REPL. Potresti perdere un po' della reattività che altri cicli hanno, ma guadagni la capacità di lavorare programmaticamente con il contesto. La cosa fondamentale qui è che gli script nel REPL sono effimeri. Ottieni un runtime di scripting e una gestione del contesto, ma non c'è riutilizzabilità o componibilità. Basta scrivere lo script, eseguirlo e scompare. In termini di costruzione di sistemi, questo è decisamente peggio che concatenare agenti e file markdown con script bash perché perdi la persistenza e l'esecuzione limitata.

Dai Cicli Individuali all'Orchestrazione su Larga Scala

Deep research di OpenAI[[6]](http://localhost:5173/blog/onyx#ref-6) è stato uno dei primi esempi di un flusso di lavoro deterministico che aveva una forma o schema di esecuzione generale con una piccola variabilità esecuzione per esecuzione. Funziona pianificando un lotto di query, eseguendole sul web, rivedendo i risultati e pianificando il lotto successivo di query. Ogni lotto approfondisce lo spazio del problema.

akira - inline image

Cursor ha portato l'idea del determinismo molto oltre quando @wilsonzlin ha dimostrato un'infrastruttura che orchestrava agenti per costruire un browser. Ha costruito un'infrastruttura su misura per coordinare grandi quantità di lavoro utilizzando agenti pianificatori paralleli e agenti di compito [[7]](http://localhost:5173/blog/onyx#ref-7). Ciò che è rilevante qui è che la relazione tra ogni parte dell'infrastruttura è fissa. Ci sono pianificatori, che esplorano lo stato attuale del sistema e generano compiti, ed esecutori, che prendono i compiti e li implementano in parallelo. Ci sono binari fissi tra gli agenti e canali fissi per comunicare informazioni. Per fare un buon coordinamento, hai bisogno di garanzie sulle interfacce.

Usare le Condizioni di Terminazione per l'Esecuzione Limitata

A maggio, Codex ha introdotto l'idea di un obiettivo che utilizza un ciclo di verifica per scalare verso uno stato finale desiderato fino al completamento di un compito. Puoi pensarlo come una versione pronta per la produzione del Ralph loop, integrata in Codex. Ti permette di specificare un obiettivo e ha un ciclo automatizzato che esegue e rivede, integrato.

akira - inline image

Autoresearch di Karpathy[[9]](http://localhost:5173/blog/onyx#ref-9) è simile a /goal di Codex e al Ralph loop. Combina la condizione di terminazione verificabile dell'obiettivo con la limitazione dell'esecuzione di un Ralph loop sulle iterazioni, permettendogli di progredire continuamente verso un obiettivo. Fa progressi esplorando lo spazio delle idee, migliorando iterativamente nel tempo.

akira - inline image

Fino a questo punto, tutte le soluzioni che esternalizzano l'orchestrazione al di fuori dell'agente sono fisse nella forma del loro grafo di esecuzione. Eseguono usando uno schema scritto a mano e hanno una sorta di schema per le forme consentite in cui possono operare. Non si adattano per compito o non hanno forti garanzie sulla forma del grafo di esecuzione.

Rendere l'Orchestrazione Flessibile

A marzo di quest'anno, abbiamo presentato Slate, il primo agente di codifica a utilizzare il codice per l'orchestrazione live di sottoagenti nello stile di RLM. È ancora l'unico agente di codifica ampiamente utilizzato che usa il codice per fare orchestrazione live di agenti. In Slate, i thread possono essere generati, messi in pausa, ripresi e guidati in tempo reale. L'agente principale capisce profondamente come orchestrare tutti i sottoagenti in esecuzione in modo che tu non debba farlo. Tuttavia, simile a RLM, eravamo ancora di fronte alla sfida di condividere lo stato tra sottoagenti e scripting effimero, cosa che non incontreresti usando uno script bash e un file markdown.

Ancora di più, se il modello è quello che fa l'orchestrazione, come lo guidi? Gli dici di scrivere il suo codice di orchestrazione in un modo specifico? Cosa fai?

La nostra soluzione iniziale (come patch prima di rilasciare il runtime Onyx) si chiamava orchestration skills [[13]](http://localhost:5173/blog/onyx#ref-13). L'idea era semplice: permettere all'utente di fornire una skill per guidare come l'agente affronta la sua orchestrazione. Tutto qui. Funzionava abbastanza bene, ma aveva molti problemi.

In particolare, una skill non è un contratto comportamentale vincolante. Non puoi ottenere una garanzia dal testo.

Ciò significa che l'orchestratore non doveva seguire lo schema di esecuzione desiderato perché non c'era un modo reale per imporlo. Uno dei maggiori vantaggi del runtime Onyx è che abbiamo risolto questo problema.

Nessuno dei sistemi menzionati ha contratti comportamentali vincolanti.

Bene, allora, e se l'agente potesse scrivere il suo codice di orchestrazione in uno script per ogni compito in modo che il grafo di esecuzione sia fisso? Questo è ciò che sono i Claude dynamic workflows.[[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) Allo stesso modo di RLM e Slate, scrivendo codice per orchestrare sottoagenti, i dynamic workflows permettono a Claude di scrivere e salvare forme di flusso di lavoro. Questo si combina con /loop per poter iterare su schemi specifici. Fornisce un contratto dichiarativo per il comportamento di un insieme di agenti. Non è ancora come scrivere software poiché manca di cose come la composizione funzionale, ma ottieni persistenza e una forte garanzia su come verrà eseguito il compito. Sono script di flusso di lavoro scritti dinamicamente per un dato compito ad hoc.[[12]](http://localhost:5173/blog/onyx#ref-12) E poiché sono persistenti su disco, hanno un ulteriore vantaggio: possono essere rieseguiti e avvolti con colla di orchestrazione come /loop.

akira - inline image

Se noti, tutte le soluzioni di cui sopra mirano alla stessa cosa: un modo deterministico per controllare come gli agenti eseguono nel tempo.

Questa è una storia che abbiamo già visto svolgersi nell'ingegneria del software come campo. Abbiamo iniziato incollando insieme sistemi disparati e compiti di scripting, e poi i nostri linguaggi sono diventati più flessibili e potenti. Abbiamo guadagnato sempre più leva sul processo di ingegneria con ecosistemi più forti che ci permettono di costruire sistemi più affidabili a livelli di astrazione più elevati.

In questo momento, gli agenti sono sulla stessa traiettoria, e oggi rilasciamo il passo successivo lungo quella traiettoria per permetterti di progettare i sistemi che eseguono i tuoi agenti. I linguaggi di programmazione usano spesso interpreti o VM per pianificare automaticamente le risorse. Questo è ciò che ti dà leva come ingegnere che usa il linguaggio.

Se una VM dovesse avere senso per l'orchestrazione degli agenti, avresti bisogno di alcune cose:

  1. Gestione dello stato persistente: dovremmo essere in grado di definire lo stato, referenziarlo per nome, persisterlo e manipolarlo programmaticamente.
  1. Garanzie di tipo. Dovremmo rispettare le forme di input e output definite e seguirle, ed essere in grado di fare affidamento su di esse.
  1. Primitive di flusso di controllo, preferibilmente quelle ben note che un LLM capirebbe.
  1. Struttura chiara per la gestione degli errori (es. try-catch).
  1. Gestione delle risorse: controlli definiti su risorse come parallelismo degli agenti, costo, quali modelli sono in esecuzione, ecc.
  1. Isolamento dell'esecuzione: un dato agente o programma in esecuzione dovrebbe essere isolato da un altro a meno che lo stato non sia esplicitamente condiviso.
  1. Controllo del ciclo di vita: come appare un programma agente e la semantica per eseguire, cancellare e guidare. Senza questo, non hai un percorso chiaro per la pulizia e non puoi controllare la gestione del ciclo di vita.
  1. Componibilità: i programmi dovrebbero comporsi l'uno nell'altro e dovrebbero essere chiamabili con tipi di input e output definiti.
  1. Visibilità: Dovremmo essere in grado di sapere cosa è stato eseguito, quando, e dovremmo essere in grado di tracciare un fallimento di esecuzione nella fonte.
  1. Durabilità: Dovremmo avere un modello chiaro su come possiamo riprenderci da crash e riprendere.

Ognuno di questi è un problema che è già stato risolto dai linguaggi di programmazione decenni fa. L'orchestrazione degli agenti li sta semplicemente incontrando tutti di nuovo per la prima volta.

Per essere veramente in grado di scrivere software per questo, un programma "program.ts" deve essere creato in un runtime che supporti tutto quanto sopra, in modo da poter ragionare su cosa accadrà quando un programma non funziona e progettare attorno al fallimento.

Questo è il motivo per cui abbiamo costruito Onyx. È una VM per l'orchestrazione di agenti progettata precisamente per supportare sia programmi componibili persistenti che un livello di scripting interpretato. Ecco come funziona e cosa deve supportare un runtime compatibile con "program.ts".

Progettare il Runtime

Quando progettiamo un linguaggio e un runtime per quel linguaggio, dobbiamo pensare ai vincoli su cui vogliamo essere in grado di ragionare e a ciò che ci interessa sia facilmente esprimibile. Poi possiamo suddividere la semantica risultante in due categorie: semantica statica e semantica runtime.

La semantica statica sono tutte le cose che possono essere dedotte su un programma semplicemente guardandolo. Le cose che un compilatore o un type checker sanno su un dato programma.

La semantica runtime definisce cosa il codice significa effettivamente e come il programma viene effettivamente eseguito. Questo include i meccanismi sottostanti di allocazione delle risorse e pianificazione.

Il nostro obiettivo con un runtime per agenti è trasformare il flusso di controllo dell'orchestrazione in codice, e vogliamo rendere lo stato di esecuzione persistente e tipizzato in modo da poterlo usare in modo affidabile per guidare l'orchestrazione.

Alcuni requisiti della VM

Ci sono 3 cose specifiche della VM di cui ci preoccupiamo oltre a qualcosa come la normale esecuzione TypeScript.

  1. Come runtime di orchestrazione di agenti, deve essere in grado di orchestrare agenti. Ciò significa crearli, tracciare i loro cicli di vita, ecc. Vogliamo che il runtime sia in grado di eseguirli in modo bloccante o non bloccante e pianificarli correttamente.
  1. Vogliamo il controllo sulle forme di output degli agenti e vogliamo un'applicazione rigorosa del contratto di output.
  1. Vogliamo avere il controllo runtime su risorse esterne come modelli e costi.

Eseguire Agenti e Programmi

Per eseguire un agente, abbiamo selezionato due verbi di base: run e spawn. Run esegue un agente bloccante in primo piano. Spawn esegue un agente in background. Questo è in linea con le comprensioni comuni di spawn, come posix_spawn, rendendo facile per un modello capire i nostri nuovi verbi poiché sono concettualmente nei dati di addestramento. Spawn e run ti permettono di invocare direttamente agenti e programmi letti dal disco, restituendo informazioni sufficienti per un handle di esecuzione.

Run supporta anche alcune cose. Supporta tipi di output direttamente applicati tramite zod @colinhacks, e supporta override diretti del modello, rendendo facile scrivere ed eseguire programmi in cui ha senso diramarsi a più modelli diversi per soluzioni diverse o fasi diverse di un compito.

typescript
1function run<S extends z.ZodType>(
2 name: string,
3 options: ...
4): Promise<z.infer<S>>

Run ti permette di concatenare direttamente sottoagenti inline.

typescript
1// esecuzione agente semplice
2const out = await run({ type: "read", prompt: () => "Rispondi con: ok" })
3// esecuzione nominata (string = child workflowId)
4const review = await run("reviewer", {
5 type: "general",
6 prompt: () => "Revisiona il diff",
7})
8// output strutturato (risultato tipizzato)
9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })
10const v = await run({
11 type: "general",
12 prompt: () => "Valuta il rischio",
13 output: Verdict,
14})

Spawn è simile a run ma crea un agente in background. I sottoagenti generati non vengono attesi e il flusso di controllo procede. Spawn è molto utile per avviare diversi agenti di esecuzione non bloccanti.

typescript
1// agente in background
2const h = await spawn("worker", { type: "general", prompt: "Compito lungo" })

Interagire con Agenti in Esecuzione

Vogliamo essere in grado di eseguire due tipi di operazioni sugli agenti in esecuzione: guidare e fermare.

Un messaggio di guida è un messaggio inviato all'agente che l'LLM riceverà mentre è in esecuzione per spingerlo in una direzione. Questo è utile per aggiornare il contesto del compito dell'agente senza dover smantellare il worker.

Anche la cancellazione è importante: vogliamo essere in grado di smantellare attivamente un sottoagente se non dovrebbe essere in esecuzione.

Essere in grado di eseguire queste operazioni sia dal REPL live che da un programma pre-autorizzato dà a Slate la sua capacità di orchestrare tutto in tempo reale. Può definire dinamicamente la forma dell'orchestrazione in fase di runtime, oppure può creare e iterare su software reale per fare l'orchestrazione.

Slate è in grado di scrivere programmi in file \.program.ts. Un file di programma ha alcune cose: il suo nome (è così che Slate sa cos'è), una descrizione JSDoc, e poi il corpo del programma vero e proprio*. Una dichiarazione di programma appare così:

typescript
1program(async (ctx) => {
2 // modello economico per la ricerca — deve solo trovare file
3 const findings = await run("search", {
4 type: "read",
5 prompt: "Trova tutti i file relativi all'autenticazione",
6 model: "codex/gpt-4.1-mini", // usa la tua chiave codex integrata
7 })
8})

I programmi seguono lo stesso modello di esecuzione asincrona che ci permette di eseguire un programma sia in primo piano che in background, e interagire con esso mentre è in esecuzione.

typescript
1// agente in background
2const h = await spawn("worker", { type: "general", prompt: "Compito lungo" })
3await h.notify("concentrati prima sul parser") // messaggio di guida all'agente in esecuzione
4const result = await h.result() // attendi il completamento più tardi
5// diramazione, poi raccolta
6const a = await spawn({ prompt: "compito A" })
7const b = await spawn({ prompt: "compito B" })
8const [ra, rb] = [await a.result(), await b.result()]
9// programma in background
10import Audit from "deep-audit"
11const ah = await spawn(Audit, { input: { pr: 42 } })
12const auditResult = await ah.result()

Output Strutturato e Stato

Questo è un limite primario di ogni altro sistema fino ad oggi. Lo stato, in tutti gli altri sistemi, è scarsamente esternalizzato e non è isolato in modo sicuro. Se è un file sul sistema, non puoi garantire l'assenza di corruzione. Se puoi, non puoi comunque garantire l'analizzabilità. Non puoi sottoscrivere i cambiamenti di stato per guidare le operazioni e non puoi garantire l'aderenza al tipo.

Ricordi come volevamo uno stato persistente che fosse anche strutturato e potesse essere referenziato?

Lo stato, in Onyx, è diverso. I namespace di stato sono dichiarati, nominati direttamente e persistenti nel tempo. Ciò significa che un archivio di stato può essere riutilizzato più e più volte, permettendoti di costruire sistemi di agenti a lunga esecuzione con dati reali.

Sia gli agenti che il codice leggono lo stato, e il determinismo che volevamo da un runtime deriva da questo. Gli agenti leggono lo stato attraverso uno strumento dedicato che permette loro di interagire sempre con esso in modo sicuro e strutturato. Gli agenti e i programmi sono entrambi consumatori che possono essere guidati per modificare lo stato, il che permette al runtime di fare affidamento sull'oggetto stato per guidare l'orchestrazione.

Lo stato e l'aderenza allo schema bloccano il completamento del sottoagente. Per questo motivo, lo stato fornisce una superficie unificata per guidare l'intero programma.

Gli oggetti stato possono anche essere passati come variabili runtime nelle sessioni figlie condivise con l'agente principale. Questo accesso per riferimento attraverso la gerarchia degli agenti (che è una novità assoluta) permette la comunicazione tra agenti attraverso un canale di stato condiviso.

akira - inline image

Cicli a Lunga Esecuzione

Alcuni programmi devono funzionare più come sistemi in esecuzione. Prendi openclaw, per esempio. Puoi effettivamente rappresentare openclaw come un programma date le giuste primitive. Per questo, usiamo due primitive: sleep e checkpoint.

Sleep fa quello che ti aspetteresti: dorme.

Ora, ecco il punto: diciamo che vuoi una qualche gestione di compiti a lunga esecuzione in background. Un grafo di esecuzione predefinito potrebbe bloccarsi o rompersi, ed è quindi importante che l'agente principale sia a conoscenza dello stato del programma.

Per supportare questo, introduciamo la primitiva checkpoint.

Un checkpoint può essere qualsiasi cosa, ma la ragione per cui si chiama checkpoint è perché notifica l'agente principale con un oggetto di forma fissa. Questo permette all'agente principale di tracciare cose come l'avanzamento del compito e di essere notificato direttamente sui cambiamenti nello stato del programma. A sua volta, l'agente principale può quindi gestire più efficacemente un programma in esecuzione.

Onyx supporta la creazione di un ciclo agente come Openclaw, cioè un agente persistente con un battito cardiaco.

Questo è davvero interessante: puoi comporre i primitivi in un tipo di agente completamente diverso semplicemente usando un ciclo while, una pausa e un checkpoint.

Openclaw può essere semplicemente rappresentato come un file di programma!

typescript
1// Un programma per eseguire un ciclo di auto-ricerca a lunga esecuzione
2for (let i = 0; i < maxExperiments; i++) {
3 const idea = await run("propose", { ... })
4 const result = await run("train", { ... })
5 checkpoint({ message: `esperimento ${i}`, data: { idea, result } })
6 await sleep(30_000) // pausa tra gli esperimenti
7}
8
9// Un programma per eseguire un agente persistente in stile openclaw
10while(true) {
11 const status = await run("status_check", { ...inserisci modello economico qui... })if(status.pending_tasks) {checkpoint({ tasks: status.pending_tasks }) // restituisci lo stato importante e risveglia l'agente principale}
12 await sleep(30_000) // pausa tra gli esperimenti
13}

Composizione

Con Onyx, Slate può scrivere un file *.program.ts per te. Questo persiste e può (e dovrebbe) essere trattato come codice normale. Ha tipi integrati già pronti, viene eseguito in un runtime privo di variabili globali, ed è semplicemente TypeScript, quindi il suo modello di composizione è semplicemente importare e chiamare un altro programma.

Poiché è solo TypeScript, ottieni cose come il parallelismo (Promise.all) e i cicli gratuitamente.

Ecco come importeresti un programma e lo useresti in un altro:

typescript
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", ... output dell'audit) // questo eseguirebbe e correggerebbe l'output del programma di audit.
4})

Semantica degli errori

Gli errori, nella VM ideale, vengono sollevati in modo esplicito. Dovrebbero essere sollevati per problemi di sintassi a runtime, fallimenti degli agenti, crash, ecc.

Nello specifico, definiamo gli errori di orchestrazione come:

  • Un agente è bloccato su un'attività
  • Un agente non è riuscito a completare un'attività
  • Un agente ha esaurito i passaggi o il budget per un'attività
  • Un programma ha esaurito il budget per un'esecuzione
  • Il modello di orchestrazione non è riuscito a scrivere codice sintatticamente corretto
  • Una modifica illegale dello stato

Tutti questi casi di errore specifici definiscono la semantica del runtime. Dicono: "Puoi aspettarti che questo runtime sollevi un'eccezione, perché consideriamo un fallimento dell'esecuzione dell'agente allo stesso modo di un errore nel codice". All'inizio potrebbe sembrare fastidioso, ma questo meccanismo di fallimento esplicito ti dà qualcosa in cambio: un modo esplicito per prepararsi e programmare attorno ai fallimenti. Quindi, in realtà, ti dà più controllo, non meno.

typescript
1// gli errori sono try/catch — come in qualsiasi programma TypeScript
2program(async (ctx) => {
3 try {
4 const result = await run("risky-refactor", {
5 type: "general",
6 prompt: "Refactoring del modulo di autenticazione",
7 model: "claude-sonnet",
8 maxSteps: 20,
9 })
10 } catch (err) {
11 // l'agente ha fallito — ma sappiamo esattamente perché.
12 // la traccia ha ogni chiamata di strumento, ogni richiesta al modello,
13 // ogni scrittura di stato che ha portato qui.
14
15 // riprova con un modello diverso
16 const result = await run("risky-refactor-retry", {
17 type: "general",
18 prompt: `Il tentativo precedente è fallito: ${err.message}. Prova un approccio diverso.`,
19 model: "claude-opus",
20 maxSteps: 30,
21 })
22 }
23})

Selezione del modello, controllo del budget e BYOK

La selezione del modello integrata ti consente di avere un controllo ancora più preciso. La skill /models dà a Slate accesso completo all'elenco dei modelli disponibili, permettendo a Slate di creare programmi con più modelli diversi che svolgono lavori differenti. Vuoi che Fable sia il pianificatore, ma GLM 5.2 a implementare all'interno di un ambiente deterministico? Certo. Vuoi distribuire una domanda su Gemini, GPT 5.5 e DeepSeek? Funziona anche quello.

Inoltre, il runtime supporta due tipi di override di configurazione per i programmi:

  • I modelli globali predefiniti utilizzati per l'esecuzione dell'agente
  • Il budget per eseguire un programma

Puoi impostare direttamente un budget di esecuzione per limitare la spesa per un determinato ciclo.

Inoltre, il runtime supporta l'utilizzo dei tuoi abbonamenti esistenti a OpenAI e Github Copilot.

typescript
1program(async (ctx) => {
2 // modello economico per la ricerca — deve solo trovare file
3 const findings = await run("search", {
4 type: "read",
5 prompt: "Trova tutti i file relativi all'autenticazione",
6 model: "codex/gpt-4.1-mini", // usa la tua chiave codex integrata
7 })
8
9 // modello di ragionamento per la parte difficile — deve pensare
10 const plan = await run("architect", {
11 type: "general",
12 prompt: `Progetta una correzione basata su: ${findings.output}`,
13 model: "openai/o3", // Utilizza i crediti API
14 output: z.object({
15 approach: z.string(),
16 files: z.array(z.string()),
17 risk: z.enum(["low", "medium", "high"]),
18 }),
19 })
20
21 // modello di fascia media per l'implementazione — deve solo modificare
22 const handles = await Promise.all(
23 plan.files.map(f => spawn("fix-" + f, {
24 type: "general",
25 prompt: `Applica questa correzione 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})

Definire la superficie di authoring

Ci sono stati due fattori principali nella progettazione della superficie di authoring per i programmi: quanto è facile per un agente comprenderla e quanto è facile per un umano leggerla. Abbiamo scelto verbi relativamente semplici che suonano come l'inglese e abbiamo deciso esplicitamente di voler modellare l'orchestrazione in modo procedurale piuttosto che dichiarativo.

Anche la selezione di TypeScript come linguaggio è stata importante. C'è così tanto codice TypeScript procedurale in circolazione che un modello capirà implicitamente la semantica di TypeScript, anche senza post-training.

akira - inline image

Componenti ingegneristici della nostra fabbrica software

La prossima domanda a cui rispondere è: cosa ti offre tutto questo?

Ti offre la possibilità di scrivere software reale per la tua orchestrazione degli agenti. Ora puoi progettare la tua orchestrazione degli agenti dall'inizio alla fine.

Puoi progettare la fabbrica.

Ad esempio, puoi creare un programma che monitora Github in un ciclo e un programma separato che esegue un agente di implementazione con un agente di QA per la revisione. Entrambi sono pattern individualmente utili che potresti incontrare. Poi puoi metterli insieme per creare un sistema che ascolta i commenti su una PR, genera un implementatore per rispondere a quei commenti, e poi genera un agente di QA per assicurarsi che la correzione sia valida.

Puoi quindi utilizzare questo programma collegato a una coda di attività per delegare e monitorare il lavoro sul tuo codebase e fargli rispondere automaticamente ai commenti delle PR.

E puoi fare tutto questo usando modelli open weight veloci. Poiché è solo codice, non hai bisogno di un LLM potente per pensare all'orchestrazione dopo che è stata creata la prima volta.

Ora arriva la parte divertente: è tempo di condividere alcuni dei programmi che abbiamo utilizzato per aumenti massicci della produttività.

Ricerca Approfondita del Codebase

Usiamo questo programma per aiutare a definire l'ambito delle attività. Esegue una ricerca approfondita sullo stato del nostro monorepo e prepara un pacchetto di ricerca a cui un implementatore può fare riferimento. Lo usiamo continuamente. Sembra costoso, ma in realtà non lo è. Puoi eseguire questo programma in Slate con DeepSeek V4 Flash e il processo di ricerca è approfondito ma estremamente economico.

akira - inline image

Obiettivo-Review-PR

Questo è quello che usiamo per implementare un'attività una volta completata la ricerca. Fortunatamente, quando la ricerca raggiunge il programma obiettivo, la maggior parte dell'ambiguità dell'attività è stata risolta, il che rende l'esecuzione dell'attività ancora più veloce. Caricare la ricerca con un modello OSS leggero ci permette di utilizzare un modello costoso come Opus per ciò che conta: scrivere codice veramente buono e verificare lo stato del sistema. Potresti persino modificare il programma per usare GPT 5.5 per rivedere in modo avversariale il lavoro di Opus 4.8.

akira - inline image

Auto-ricerca come Programma

L'auto-ricerca[[9]](http://localhost:5173/blog/onyx#ref-9) era originariamente interamente guidata da LLM. Si dirigeva un agente verso il prompt program.md e lui decideva cosa provare e come progredire.

Non sorprende che l'auto-ricerca sia in realtà solo un programma.

I programmi agente ti permettono di invertire questo processo e mettere il flusso di controllo nel runtime. Il programma possiede il flusso di controllo mentre gli agenti svolgono il lavoro con effetti collaterali (modificare il codice, eseguire git, fare SSH sulla GPU remota, addestrare). Per il programma di auto-ricerca, la decisione di mantenere/ripristinare è codice deterministico:

typescript
1kept = status === "ok" && valBpb != null && valBpb < best

Nel nostro caso, il programma esegue un agente di configurazione per preparare un nuovo repo e verificare che la A100 remota sia raggiungibile. Se la configurazione fallisce, termina anticipatamente con un'uscita pulita basata su un valore tipizzato. Altrimenti, entra nel ciclo degli esperimenti.

Ogni esperimento ottiene un agente nuovo. All'agente vengono forniti la configurazione migliore corrente e la cronologia delle idee e dei risultati precedenti, in modo che non si ripeta e possa basarsi su ciò che è stato mantenuto. Propone una modifica, modifica train.py, esegue il commit, sincronizza con rsync sulla macchina remota, addestra e classifica il risultato.

L'agente e il programma condividono lo stato. L'agente scrive i dati nello stato e il programma valuta lo stato per il flusso di controllo. In base al risultato, un agente registrar aggiorna results.tsv e, opzionalmente, resetta l'esecuzione se il programma ha deciso di scartare l'esperimento. Questo lascia il git HEAD sempre puntante al ramo migliore corrente dell'albero degli esperimenti.

Ci sono due differenze fondamentali a cui vale la pena prestare attenzione: 1) questo viene eseguito in un programma, quindi possiamo generare un agente nuovo per ogni esperimento e 2) possiamo decidere quale attività l'agente dovrebbe svolgere in base allo stato live del programma.

akira - inline image
akira - inline image

Ed ecco come appare nel codice:

typescript
1// ---------- Programma ----------
2
3program(async (ctx) => {
4 const c = cfg(ctx.input)
5 const total = ctx.input?.maxExperiments ?? 20
6
7 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: `configurazione fallita: ${setup.note}`, setup }
15 }
16
17 let best = c.baselineValBpb
18 let bestCommit = setup.baselineCommit
19 const history = []
20
21 for (let i = 1; i <= total; i++) {
22 let exp
23 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 // Agente in errore/bloccato — trattalo come un crash, ripristina il repo al migliore, continua.
32 exp = {
33 description: `esperimento ${i} errore 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 }
44
45 const kept = exp.status === "ok" && exp.valBpb != null && exp.valBpb < best
46
47 await run(`ar-record-${i}`, {
48 prompt: recordPrompt(c, exp, kept, bestCommit),
49 type: "general",
50 maxSteps: 20,
51 output: RecordResult,
52 })
53
54 if (kept) {
55 best = exp.valBpb
56 bestCommit = exp.commit
57 }
58
59 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 })
68
69 await checkpoint({
70 name: `experiment-${i}`,
71 message: `exp ${i}/${total}: ${exp.status}${kept ? " MANTENUTO" : ""} val_bpb=${exp.valBpb ?? "n/a"} (best=${best})`,
72 data: { i, total, status: exp.status, valBpb: exp.valBpb, kept, best, bestCommit },
73 })
74 }
75
76 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})

Lavori futuri

L'unico requisito rimanente della VM che non abbiamo ancora definito è il modello di durabilità per i programmi. Non è chiaro quale sia il modello corretto per riprendere e gestire il ciclo di vita di un programma, e quale livello di controllo dovrebbe essere esposto sul runtime.

Oltre a questo, ci sono così tante cose entusiasmanti che aggiungeremo per supportare diversi carichi di lavoro e forme di attività, in modo da poter scrivere software reale per orchestrare meglio gli agenti. Siamo certi che molti dei pattern emergeranno dalle persone che useranno i programmi in modi creativi per conto proprio.

Non vediamo l'ora di vedere cosa costruirai.

  • Il Team RL

Riferimenti

  1. Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
  2. Geoffrey Huntley, "The Ralph Loop"
  3. Geoffrey Huntley, "everything is a ralph loop," January 2026
  4. Zhang, Kraska, Khattab, "Recursive Language Models," December 2025
  5. Wang et al., "Executable Code Actions Elicit Better LLM Agents," ICML 2024
  6. OpenAI, "Introducing Deep Research," February 2025
  7. Cursor, "Scaling Agents," January 2026
  8. OpenAI, "Using Goals in Codex"
  9. Andrej Karpathy, "autoresearch"
  10. Anthropic, "Introducing Dynamic Workflows in Claude Code"
  11. OpenAI, "Learning to Reason with LLMs," September 2024
  12. Anthropic, "A harness for every task: dynamic workflows in Claude Code"
  13. Random Labs, "Skill Chaining"

Turn one viral article into a full content workflow

Collect the source, decode the pattern, create assets, draft the story, and distribute from one AI workspace.

Explore YouMind
Per i creator

Trasforma il tuo Markdown in un articolo 𝕏 pulito

Quando pubblichi i tuoi testi lunghi, formattare immagini, tabelle e blocchi di codice per 𝕏 è una seccatura. YouMind trasforma un'intera bozza Markdown in un articolo 𝕏 pulito e pronto da pubblicare.

Prova Markdown verso 𝕏

Altri pattern da decodificare

Articoli virali recenti

Esplora altri articoli virali