Twenty years ago, I was an associate at Kinoma, in California. We were building software for tiny devices, and at the heart of it was a JavaScript engine designed to run where nothing else would fit: XS. It was small, strict, and elegant — the work of Patrick Soquet, who is still its architect today.
Kinoma is gone, but XS is not. It lives on at Moddable, the company Peter Hoddie and Patrick founded, where it powers connected objects all over the world. I used it once more in between, to ship Frigo Magic, a consumer app written entirely in XS. Then I moved on, as one does.
Last winter I started building a Mac app — a private AI wiki, where you ask questions about your own documents and everything stays on your machine. Early on I hit a question every AI app hits: what runs the agents? Not the model — the agent: the small program that decides which tool to call, keeps state between turns, remembers what it learned, and wakes up on schedule to do it again.
The existing answers all looked the same. OpenClaw needs Node. Hermes needs Python. Fine for a server; wrong for a native Mac app that promises "nothing leaves your machine" and can't reasonably ship a 100 MB runtime to run a ten-line script.
Then I remembered XS.
An agent is a module
Here is a complete KaozKit agent:
// agent.js — runs inside the engine
export async function run(input) {
const reply = await host.llm.chat(
[{ role: "user", content: input.question }],
{ tools: ["current_datetime", "web_search"] } // the model may call these
);
await host.memory.save("last question", input.question);
return { answer: reply };
}
That's it. A JS module exports run(input). It talks to a language model through host.llm.chat, which runs the whole tool-call loop internally — the model asks for a tool, Swift executes it, the model continues — and resolves with the final text. It reads and writes memory. It returns JSON to the host.
Run it from the command line:
kaoz agent.js --provider apple --input '{"question":"what day is it?"}'
With --provider apple, this runs on Apple Intelligence — fully on-device, no API key, no network. Swap in --provider anthropic or --provider mlx and the agent doesn't change; the host does.
The host global is the entire capability surface of an agent. There is no fetch, no file system, no require. An agent can only do what the host explicitly exposes: host.llm, host.provider(id), host.tool.call, host.memory, host.schedule. API keys are resolved on the Swift side and never enter JavaScript. Module resolution is confined to registered roots. If you have spent time thinking about what an untrusted script should be allowed to do, this is the part that lets you sleep.
Why XS, and not JavaScriptCore?
Fair question — JSC ships with every Mac. I started with it. I switched for four reasons, each of which turned out to be a product feature rather than an implementation detail.
Heap snapshots. XS can serialize its entire heap to bytes and restore it in a fresh process. Not "serialize your state to JSON" — the heap: every object, every closure, every pending timer, the conversation an agent is in the middle of. In KaozKit this is writeSnapshot() / init(snapshot:). The consequence took me a while to fully appreciate: a resident agent's brain is a file. Kill the process, copy the file to another machine, restore — the agent picks up mid-thought. I wrote zero lines of serialization code, and I never will.
One machine per agent. XS was built for microcontrollers. A machine costs very little memory, which makes "one isolated engine per agent" a reasonable architecture rather than a luxury. An orchestrator can spawn sub-agents as separate XS machines (new Thread + new Service, with calls marshalled across the boundary) and keep several resident agents alive side by side. With JSC, I would have been counting contexts.
Confinement by construction. XS has no ambient authority. There is nothing to sandbox away — you add capabilities, one host function at a time, in C, against the classic xs.h API. The Swift side implements them; the C side is a thin shim that hands work over and settles promises when Swift is done. It is the same discipline embedded developers use to expose a sensor to a script, applied to exposing a language model.
A clean async bridge. The engine runs on a private thread; Swift work runs off it; await continuations settle correctly across the boundary. This was the hard part, and it is where Patrick's help was decisive — some of the machinery that makes it clean is now in KaozKit's C layer, and some of it is simply knowing which invariants XS holds and which it doesn't.
If you only need to evaluate a script, JSC is fine. If you want stateful, restartable, confined agents, that's what KaozKit is for.
What the snapshot changes in practice
The first thing I built once snapshots worked was a resident agent I could talk to, kill, and talk to again:
kaoz concierge.js --resident --daemon --state brain.bin --provider anthropic
You send it JSON lines on stdin. It remembers what you said. You ask it to remind you of something in twenty minutes — that arms a timer via host.schedule. Then you Ctrl-C it, look at brain.bin, and restart it with the same command. It answers "what were we talking about?" correctly, and twenty minutes after the original request, the reminder fires — in a process that didn't exist when it was set.
I showed this to Peter and Patrick a few weeks ago. There is a particular kind of pleasure in demonstrating to the authors of an engine a thing their engine can do that they had never had a reason to try.
The second thing I built was a proper actor framework on top — Agha's primitives, create / send / become, with a small LLM agent layer, in a few hundred lines of JavaScript that run entirely inside XS (even the tests). The invariant that matters there is that message handlers are synchronous: one await in the middle of a handler, and the next message interleaves with half-mutated state. Anything asynchronous — an LLM call, a tool — enters and leaves by message. And because behaviors and mailboxes live in the heap, the snapshot persists the whole actor system: a pipeline that is halfway through collecting from thirty sources survives kill -9 and resumes.
That framework now runs a daily newsletter agent on my Mac, and, since this week, the agent that prepares my morning reading. Both were built by the same rule: no Node anywhere, everything runs through kaoz.
The layers
KaozKit is a single SwiftPM package that vends products in layers, so you take only what you need:
KaozJSCore (C) — XS engine + the async-settle bridge
KaozJS — Swift XSEngine: dedicated thread, snapshot, module roots
KaozHostC (C) — the agent's host functions (host.llm / tool / memory / schedule)
KaozKit — agent runtime: providers, tools, memory, channels, persona
KaozMLX — MLX on-device inference (heavy deps, opt-in)
kaoz — headless CLI / resident daemon
If you only want a JS↔Swift engine with snapshots, KaozJS is usable on its own. If you want agents, KaozKit pulls the rest in. Providers cover Anthropic, OpenAI, Google, Mistral, DeepSeek, Ollama, LM Studio, Apple Intelligence, MLX, and ComfyUI for images — behind one LLMProvider protocol, which is what lets an agent chain them: ask Claude to write an image prompt, hand it to ComfyUI, return the picture.
Tools follow the same philosophy: read tools are safe by default and confined to authorized folders; anything that acts — writing files, running a shell command, making an HTTP request — is opt-in, per invocation, with its own allow-list. And there is a declarative plugin format: point a JSON manifest at any REST API, and it becomes a tool the model can call.
Licensing, honestly
KaozKit is MIT. The XS engine it links against is Moddable's, under LGPL v3, and it is not vendored: you supply it from your own Moddable checkout, and a script symlinks the subset the package compiles. For open-source or non-distributed use, that's all there is to it.
If you ship a closed-source binary — in particular on the Mac App Store, where the LGPL's relinking requirement isn't realistically satisfiable — you want a commercial XS license from Moddable. They offer them for exactly this case; my own app is licensed that way. I mention it up front because it is the first question I would ask.
What's next
KaozKit is the foundation of TyKaoz — ty, the house, kaoz, the conversation, in Breton — the private AI wiki I mentioned at the start, built in Rennes, launching this autumn for macOS. The library came out first on purpose: the runtime had to stand on its own before the house was built on it.
The code is at github.com/sebastien-burel/KaozKit — macOS 26+, Apple Silicon, MIT. It is young and the agent API may still move before 1.0. If you have embedded XS, built an agent runtime, or just want to argue about heap snapshots, I would genuinely like to hear from you: demat@tykaoz.bzh, or an issue on the repo.
And if you were at Kinoma — you know who you are — this one's a reunion.
Sébastien Burel builds TyKaoz at Haruni, in Rennes, France. He was an associate at Kinoma and shipped Frigo Magic entirely in XS.