Alcune settimane fa, abbiamo annunciato Dynamic Workers, una nuova funzionalità della piattaforma Workers che consente di caricare il codice Worker al volo in una sandbox sicura. L'API Dynamic Worker Loader fornisce essenzialmente accesso diretto alla primitiva di isolamento computazionale di base su cui Workers si è sempre basato: gli isolati, non i container. Gli isolati sono molto più leggeri dei container e, in quanto tali, possono essere caricati 100 volte più velocemente utilizzando 1/10 della memoria. Sono così efficienti che possono essere considerati "usa e getta": basta avviarne uno per eseguire qualche riga di codice e poi buttarlo via. Come una versione sicura di eval().
I Dynamic Workers hanno molti usi. Nell'annuncio originale, ci siamo concentrati su come utilizzarli per eseguire codice generato da agenti IA in alternativa alle chiamate a strumenti. In questo caso d'uso, un agente IA esegue azioni su richiesta dell'utente scrivendo e mettendo in esecuzione alcune righe di codice. Il codice è monouso, destinato a svolgere un'unica attività una sola volta e viene scartato immediatamente dopo l'esecuzione.
Ma cosa succede se si desidera che un'intelligenza artificiale generi codice più persistente? E se volessi che la tua IA creasse una piccola applicazione con un'interfaccia utente personalizzata con cui l'utente possa interagire? Cosa succede se si desidera che l'applicazione abbia uno stato di lunga durata? Ovviamente, il tutto eseguito in una sandbox sicura.
Un modo per farlo sarebbe utilizzare Dynamic Workers e fornire semplicemente al Worker un'API RPC che gli dia accesso allo spazio di archiviazione. Utilizzando i binding, potresti fornire al Dynamic Worker un'API che punti al tuo database SQL remoto (magari basato su Cloudflare D1 o un database Postgres a cui accedi tramite Hyperdrive, dipende da te).
Ma Workers ha anche un tipo di archiviazione unico ed estremamente veloce che potrebbe essere perfetto per questo caso d'uso: i Durable Objects. Un Durable Object è un tipo speciale di Worker che ha un nome univoco, con un'istanza a livello globale per nome. Tale istanza ha un database SQLite collegato, che risiede sul disco locale sulla macchina su cui viene eseguito il Durable Object. Ciò rende l'accesso allo spazio di archiviazione incredibilmente veloce: la latenza è effettivamente pari a zero.
Forse, quindi, ciò che desideri veramente è che la tua IA scriva del codice per un Durable Object, e poi tu voglia eseguire quel codice in un Dynamic Worker.
Questo rappresenta un problema strano. Normalmente, per utilizzare i Durable Objects devi:
Scrivere una classe che estende DurableObject.
Esportarla dal modulo principale del tuo Worker.
Specificare nella configurazione di Wrangler che lo spazio di archiviazione deve essere predisposto per questa classe. Ciò crea un namespace di Durable Object che punta alla tua classe per la gestione delle richieste in arrivo.
Dichiara un binding del namespace di Durable Object che punta al tuo namespace (o usa ctx.exports), e usalo per effettuare richieste al tuo Durable Object.
Tutto questo non si estende naturalmente ai Dynamic Workers. Innanzitutto, c'è il problema ovvio: il codice è dinamico. Lo si esegue senza invocare affatto l'API Cloudflare. Ma l'archiviazione di Durable Object deve essere fornita tramite l'API e il namespace deve puntare a una classe di implementazione. Non può puntare al tuo Dynamic Worker.
C'è inoltre un problema più grande: anche se fosse possibile configurare in qualche modo il namespace di un Durable Object in modo che punti direttamente a un Dynamic Worker, lo vorresti fare? Vuoi che il tuo agente (o utente) sia in grado di creare un intero namespace pieno di Durable Objects? Desideri usufruire di uno spazio di archiviazione illimitato distribuito in tutto il mondo?
Probabilmente no. Probabilmente vuoi un po' di controllo. Potresti voler limitare, o almeno tenere traccia, del numero di oggetti che vengono creati. Forse è meglio limitarli a un solo oggetto (probabilmente sufficiente per app personali con codifica a stato d'animo). Potresti voler aggiungere la registrazione e altra osservabilità. Metriche. Fatturazione. E altro ancora.
Per fare tutto questo, quello che vuoi davvero è che le richieste a questi Durable Objects vadano prima al tuo codice, dove puoi quindi eseguire tutta la "logistica" e poi inoltrare la richiesta nel codice dell'agente. Vuoi scrivere un supervisor che venga eseguito come parte di ogni Durable Object.
Soluzione: i Durable Object Facets
Oggi rilasciamo, in versione beta aperta, una funzionalità che risolve questo problema.
I Durable Object Facets consentono di caricare e istanziare una classe di Durable Object in modo dinamico, fornendo al contempo un database SQLite da utilizzare per l'archiviazione. Con Facets:
Per prima cosa viene creato un normale namespace di Durable Object, che punta a una classe che tu scrivi.
In quella classe, si carica il codice dell'agente come un Dynamic Worker e lo si richiama.
Il codice del Dynamic Worker può implementare direttamente una classe Durable Object, cioè esporta letteralmente una classe dichiarata come extends DurableObject.
In questo modo viene creata un'istanza di quella classe come "facet" del tuo Durable Object.
Il facet ottiene un proprio database SQLite, che può utilizzare tramite le normali API di archiviazione di Durable Object. Questo database è separato dal database del supervisore, ma i due sono archiviati insieme come parte dello stesso Durable Object complessivo.
Ecco una semplice e completa implementazione di una piattaforma per app che carica ed esegue dinamicamente una classe Durable Object:
import { DurableObject } from "cloudflare:workers";
// For the purpose of this example, we'll use this static
// application code, but in the real world this might be generated
// by AI (or even, perhaps, a human user).
const AGENT_CODE = `
import { DurableObject } from "cloudflare:workers";
// Simple app that remembers how many times it has been invoked
// and returns it.
export class App extends DurableObject {
fetch(request) {
// We use storage.kv here for simplicity, but storage.sql is
// also available. Both are backed by SQLite.
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
return new Response("You've made " + counter + " requests.\\n");
}
}
`;
// AppRunner is a Durable Object you write that is responsible for
// dynamically loading applications and delivering requests to them.
// Each instance of AppRunner contains a different app.
export class AppRunner extends DurableObject {
async fetch(request) {
// We've received an HTTP request, which we want to forward into
// the app.
// The app itself runs as a child facet named "app". One Durable
// Object can have any number of facets (subject to storage limits)
// with different names, but in this case we have only one. Call
// this.ctx.facets.get() to get a stub pointing to it.
let facet = this.ctx.facets.get("app", async () => {
// If this callback is called, it means the facet hasn't
// started yet (or has hibernated). In this callback, we can
// tell the system what code we want it to load.
// Load the Dynamic Worker.
let worker = this.#loadDynamicWorker();
// Get the exported class we're interested in.
let appClass = worker.getDurableObjectClass("App");
return { class: appClass };
});
// Forward request to the facet.
// (Alternatively, you could call RPC methods here.)
return await facet.fetch(request);
}
// RPC method that a client can call to set the dynamic code
// for this app.
setCode(code) {
// Store the code in the AppRunner's SQLite storage.
// Each unique code must have a unique ID to pass to the
// Dynamic Worker Loader API, so we generate one randomly.
this.ctx.storage.kv.put("codeId", crypto.randomUUID());
this.ctx.storage.kv.put("code", code);
}
#loadDynamicWorker() {
// Use the Dynamic Worker Loader API like normal. Use get()
// rather than load() since we may load the same Worker many
// times.
let codeId = this.ctx.storage.kv.get("codeId");
return this.env.LOADER.get(codeId, async () => {
// This Worker hasn't been loaded yet. Load its code from
// our own storage.
let code = this.ctx.storage.kv.get("code");
return {
compatibilityDate: "2026-04-01",
mainModule: "worker.js",
modules: { "worker.js": code },
globalOutbound: null, // block network access
}
});
}
}
// This is a simple Workers HTTP handler that uses AppRunner.
export default {
async fetch(req, env, ctx) {
// Get the instance of AppRunner named "my-app".
// (Each name has exactly one Durable Object instance in the
// world.)
let obj = ctx.exports.AppRunner.getByName("my-app");
// Initialize it with code. (In a real use case, you'd only
// want to call this once, not on every request.)
await obj.setCode(AGENT_CODE);
// Forward the request to it.
return await obj.fetch(req);
}
}
In questo esempio:
AppRunner è un Durable Object "normale" scritto dallo sviluppatore della piattaforma (tu).
Ogni istanza di AppRunner gestisce un'applicazione. Memorizza il codice dell'app e lo carica su richiesta.
L'applicazione stessa implementa ed esporta una classe Durable Object, che la piattaforma si aspetta si chiami App.
AppRunner carica il codice dell'applicazione utilizzando i Dynamic Workers, quindi esegue il codice come Durable Object Facet.
Ogni istanza di AppRunner è un Durable Object composto da due database SQLite: uno appartenente al genitore (l'applicazione AppRunner stessa) e uno appartenente al facet (App). Questi database sono isolati: l'applicazione non può leggere il database di AppRunner, ma solo il proprio.
Per eseguire l'esempio, copia il codice sopra in un file worker.js, abbinalo al seguente wrangler.jsonc ed eseguilo in locale con npx wrangler dev.
// wrangler.jsonc for the above sample worker.
{
"compatibility_date": "2026-04-01",
"main": "worker.js",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"AppRunner"
]
}
],
"worker_loaders": [
{
"binding": "LOADER",
},
],
}
I facet sono una funzionalità di Dynamic Workers, disponibile in versione beta immediatamente per gli utenti del piano a pagamento di Workers.
Consulta la documentazione per saperne di più su Dynamic Workers e Facets.