Retrieved article excerpt
Open article · Retrieved 2026-09-15T14:22:33.199214+00:00
# KaozKit
**Autonomous LLM agents, written in JavaScript, running inside your Swift app.**
KaozKit embeds the [XS engine](https://github.com/Moddable-OpenSource/moddable) (Moddable) in a Swift package. An agent is a small JS module — `export function run(input)` — that drives a language model, calls tools, and reads/writes memory. The JS heap can be **snapshotted to disk and restored in a fresh process**, so resident agents survive app restarts with their full state.
[Platform](https://camo.githubusercontent.com/4ad041380715a7b5624a5c78450756e1f70d29abc657bf0c1e14b6716c52059c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532303236253242253230284170706c6525323053696c69636f6e292d626c7565)
[Swift](https://camo.githubusercontent.com/ebf94692d5d4523ab75b3a6604717cc2c7c5dfe54f162719ba8cc51a2cebfcd1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53776966742d362d6f72616e6765)
[License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)
```
// demo/weather.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"] } // web_search needs BRAVE_API_KEY
);
await host.memory.save("last question", input.question);
return { answer: reply };
}
```
```
export ANTHROPIC_API_KEY=…
swift run -c release kaoz demo/weather.js --provider anthropic --model claude-opus-4-8 \
--input '{"question":"what day is it?"}'
```
[The kaoz CLI running the same agent twice — first on Anthropic, then fully on-device with Apple Intelligence](https://github.com/sebastien-burel/KaozKit/blob/main/demo/kaoz-demo.gif)
*(`kaoz` here is `.build/release/kaoz` on the PATH — see [Install the kaoz CLI](https://github.com/sebastien-burel/KaozKit#install-the-kaoz-cli).)*
## Quick start
macOS 26+, Apple Silicon, Xcode 26 (Swift 6). Apple Intelligence must be enabled in System Settings for `--provider apple`. From an empty folder:
```
# 1. XS engine sources (not vendored, LGPL — see License)
git clone --depth 1 https://github.com/Moddable-OpenSource/moddable.git
export MODDABLE="$PWD/moddable"
# 2. KaozKit
git clone https://github.com/sebastien-burel/KaozKit.git
cd KaozKit
./scripts/link-moddable.sh
swift build -c release
# 3. Your first agent — fully on-device, no API key needed
swift run -c release kaoz demo/hello.js --provider apple
```
**With a cloud provider:** `export ANTHROPIC_API_KEY=…` and run the same agent with `--provider anthropic --model claude-opus-4-8`.
**Optional tools:** `web_search` needs `BRAVE_API_KEY`, `news_search` needs `NEWS_API_KEY`, `send_email`/`read_email` need `--email` plus an SMTP account (see [CLI](https://github.com/sebastien-burel/KaozKit#cli--kaoz)). Without them the tool is simply unavailable — an agent that asks for it gets a warning on stderr and carries on with the rest.
## Why XS, and not JavaScriptCore?
Fair question — JSC ships with the OS. XS earns its place with capabilities JSC doesn't have:
- **Heap snapshots.** `writeSnapshot()` serializes the entire JS heap; `init(snapshot:)` restores it in a new process. A resident agent's state, conversation, and scheduled work survive relaunches — no serialization layer to write.
- **Multi-machine services.** Agents spawn sub-agents (`new Thread` + `new Service`) as isolated XS machines with alien-marshalled calls between them.
- **Confinement by construction.** Module resolution is restricted to registered roots; the `host.*` surface is the *only* capability an agent has. Secrets never enter JS — providers are resolved and keys injected on the Swift side.
- **Small engine, tiny memory footprint.** XS was built for embedded systems, where every byte counts — a full ES2026 engine that runs in a fraction of the memory JSC needs. That's what makes *one machine per agent* a reasonable architecture: spinning up sub-agents, or keeping several resident agents alive side by side, costs very little.
- **Built to be embedded.** XS runs happily on a private thread with a clean C API, and `await` continuations settle correctly across the JS↔Swift boundary.
If you only need to evaluate scripts, JSC is fine. If you want **stateful, restartable, confined agents**, that's what KaozKit is for.
## One package, layered products
KaozKit is a single SwiftPM package that vends products in layers. A project embedding **only JavaScript** depends on `KaozJS`; an **agent** project depends on `KaozKit`, which pulls the engine in.
```
KaozJSCore (C) — XS engine + the xsService* async-settle bridge
KaozJS — Swift XSEngine (dedicated thread + CFRunLoop, snapshot, module roots)
KaozHostC (C) — the agent's XS host functions (host.llm/tool/memory/schedule)
KaozKit — agent runtime: providers, tools, memory, channels, persona
KaozMLX — MLX local-inference providers (heavy deps, opt-in)
kaoz — headless CLI / resident daemon
```
`import KaozKit` for the agent runtime; `import KaozJS` (+ `KaozJSCore`) for the bare JS↔Swift engine. **macOS 26+, Apple Silicon.**
```
.package(path: "../KaozKit"),
// agent runtime:
.target(name: "YourApp", dependencies: [
.product(name: "KaozKit", package: "KaozKit"),
.product(name: "KaozMLX", package: "KaozKit"), // optional: on-device MLX
]),
// …or just the JS engine:
.target(name: "YourEngine", dependencies: [
.product(name: "KaozJS", package: "KaozKit"),
.product(name: "KaozJSCore", package: "KaozKit"), // flat C settle functions
]),
```
## Writing an agent (JS)
An agent module exports `run(input)` (or `default`). Its return value comes back to Swift as JSON. The `host` global (installed by `KaozHostC`) is the whole capability surface:
| `host.*` | Role |
| --- | --- |
| `host.llm.chat(messages, { tools }, onToken?)` | One LLM turn on the run's default provider. Runs the tool-call loop internally (the model calls a tool → Swift executes it → the model continues), resolves with the final assistant text. `onToken` streams text deltas. |
| `host.provider(id, { model, … }).chat(…)` | Same, on a specific provider from the catalog. Secrets stay in Swift — never passed from JS. |
| `host.providers()` | The provider ids/names the host exposes. |
| `host.tool.list()` / `host.tool.call(name, args)` | Enumerate / invoke a registered tool directly. |
| `host.memory.save(title, content)` / `.read(id)` / `.list()` / `.search(query, limit?)` | Persistent notes; `search` ranks by embedding similarity. |
| `host.schedule(ms, payload?)` / `host.every(ms, payload?)` / `host.cancel(handle)` | Self-scheduling: deliver a `tick` to the agent's `onTick` after / every `ms` (resident mode). |
| `host.usage()` | Cumulative `{ promptTokens, completionTokens, chatCalls }` for the run. |
| `host.snapshot(reason?)` | Ask for a checkpoint of the whole heap. Returns `false` if this host doesn't persist. |
| `host.log(…args)` | Log to the host. |
`messages` are `{ role, content }` objects (`role`: `system` / `user` / `assistant`); `tools` is an array of registered tool **names**. A **resident** agent instead exports an object of handlers — `{ onMessage, onEvent, onTick, onRestore }` — and its JS heap (state, conversation) survives across deliveries.
### Checkpoint and restore (resident)
A snapshot cannot be taken *during* a delivery: the JS stack is live and host calls may be in flight. `host.snapshot()` is therefore a **request** — the host writes as soon as the current delivery has settled. Ask for it at a point you'd be happy to wake up at:
```
onTick() { this.work(); host.snapshot("cycle done"); }
```
Coming back is the mirror image. The heap returns intact, but **timers do not**: they live in Swift and die with the process, and no module body re-evaluates to notice. So the host delivers one `restore` event to the revived agent, which re-arms from its own state — never from a stored handle, which now names a dead timer:
```
export default {
onRestore({ count, at }) { this.armFrom(this.nextRunAt); }, // optional
};
```
`restored()` (from `kaoz/host`) returns `{ count, at }` at any time, and `lastSnapshot()` reports the last checkpoint's outcome. `kaoz --state-auto` checkpoints after *every* delivery for agents that would rather not ask. See [`demo/resident-checkpoint.js`](https://github.com/sebastien-burel/KaozKit/blob/main/demo/resident-checkpoint.js).
The same capabilities are also **importable as ES modules**, so an agent can name what it uses instead of reaching for an ambient global — both forms work, and resolve to one implementation:
```
import { llm, tool, memory } from "kaoz/host"; // the surface above
import { Thread, Service } from "kaoz/thread"; // sub-agent spawn
```
Agents can compose providers freely — for example, one model writes a prompt and another renders it:
```
export async function run(input) {
const prompt = await host.provider("anthropic").chat(
[{ role: "user", content: `Write an image prompt for: ${input.idea}` }]);
const image = await host.provider("comfyui").chat(
[{ role: "user", content: prompt }]);
return { prompt, image };
}
```
An agent may also spawn **sub-agents** from the script: `new Thread(name)` + `new Service(thread, "sub-agent")`, then `await svc.method(args)` (see the engine layer below).
## Running an agent (Swift)
Two entry points in `KaozKit`:
**`AgentRuntime`** — one-shot. One engine per run, torn down when `run` finishes.
```
import KaozKit
let runtime = AgentRuntime(
makeProvider: { AnthropicProvider(apiKey: key, model: "claude-opus-4-8") },
tools: ToolRegistry(tools: [SaveMemoryTool(store: memory), /* … */]),
memory: memory, // any MemoryStoring (e.g. SemanticMemoryStore)
persona: "You are Kaoz, terse and precise.")
let json = try await runtime.run(script: source, input: ["question": "…"], timeout: 30)
// …or Moddable-style, importing the agent + its modules from disk by bare name:
let json2 = try await runtime.runRooted(
entryModule: "agent", roots: [("", agentDir.path)], input: nil, timeout: 30)
```
**`AgentHost`** — resident. One engine kept alive across many `deliver(kind:payload:)` calls; the JS heap persists, and the whole heap can be snapshotted to disk and restored in a fresh process.
```
let agent = AgentHost(entryModule: "agent", roots: [("", dir.path)],
makeProvider: …, tools: registry, memory: memory,
installThreads: false) // false ⇒ snapshot-capable
let out = try await agent.deliver(kind: "message", payload: ["text": "hi"])
let bytes = try agent.writeSnapshot() // persist state
// …later, fresh process:
let restored = AgentHost(snapshot: bytes, roots: […], makeProvider: …, tools: …, memory: …)
```
Both take a `makeProvider` (the run default), an optional `resolveProvider(id, options)` (for JS-selected providers, secrets injected in Swift), a `ProviderDescriptor` catalog, a `ToolRegistry`, a `MemoryStoring`, and optional `tokenBudget` / `persona`. `Sources/kaoz/main.swift` is the canonical worked example of wiring all of them.
## Providers
Every provider conforms to `LLMProvider` (a streaming `chat(messages:tools:)`).
- **Native (Swift):** `AnthropicProvider`, `GoogleProvider` (Gemini), `OllamaProvider`, `OpenAIProvider`, and OpenAI-compatible wrappers `LocalOpenAIProvider` (LM Studio / llama.cpp), `DeepSeekProvider`, `MistralProvider`, `QwenProvider`, `ZAIProvider`; `AppleIntelligenceProvider` (on-device Foundation Models); `ComfyUIProvider` (image generation).
- **JS-defined** (`JSProvider`, backed by `Resources/js/*.js` over the native `__http` primitive): `JSProviders.anthropic` / `.openai` / `.openaiCompatible` / `.ollama` / `.kimi` / `.google`.
- **Embeddings** (`EmbeddingProvider`): `HashingEmbeddingProvider` (dependency-free,