djs
A small JavaScript and TypeScript engine for the scripts an agent writes: programs that call tools, combine their results and return what matters. djs runs them in a WebAssembly sandbox, and it can stop a run at any instruction, write it out while it waits for a tool, and continue it later, somewhere else.
Experimental. djs is under active development. Expect bugs and changes to APIs and behavior.
Read the guideSource on GitHub
import { Sandbox } from "@earendil-works/djs";
const sandbox = await Sandbox.create({
asyncFunctions: { search: async (q) => mySearch(q as string) },
});
const count = await sandbox.runAsync(
"const hits = await search(q); return hits.length",
{ q: "small js engines" },
);
- One arena All of a run’s state (objects, strings, bytecode, call frames, pending tool calls) lives in one byte array addressed by offsets. A new VM is a copy of a prebuilt image, made in microseconds.
- Suspend anywhere The interpreter never recurses on
the native stack for JavaScript calls, so it can stop between any two instructions and
resume, also inside a getter, a
mapcallback or an async function. - Persistence A run that waits on its tools can be written out as a continuation dump: a plain JavaScript program that recreates it, in another process or a later djs.
- Cheap data passing Tool results go in through a compact wire format; results come out by reading the heap from the host, with no serialization step inside the VM.
- No syntax tree A single pass turns source into stack bytecode, the way QuickJS does. TypeScript’s types are erased while parsing.
- Determinism Time, randomness and weak references come from the host, so the same inputs give byte-identical heaps, natively and in wasm.
djs is written in no_std Rust and behaves like QuickJS, down to its error messages. It
passes 39,325 of the 39,365
test262 tests it targets, and its wasm module is 350 KB
with brotli. What djs is is the place to start;
the JavaScript interface shows how to embed it.