JavaScript LLM agents on the XS engine, embedded in Swift. Snapshots, resident agents, confined tools.
3
stars
98
commits
Swift
primary language
Sep 12, 2026
updated
Autonomous LLM agents, written in JavaScript, running inside your Swift app.
KaozKit embeds the XS engine (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.
// 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?"}'

(kaoz here is .build/release/kaoz on the PATH — see Install the kaoz CLI.)
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). Without them the tool is simply unavailable — an agent that asks for it gets a warning on stderr and carries on with the rest.
Fair question — JSC ships with the OS. XS earns its place with capabilities JSC doesn't have:
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.new Thread + new Service) as isolated XS machines with alien-marshalled calls between them.host.* surface is the only capability an agent has. Secrets never enter JS — providers are resolved and keys injected on the Swift side.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.
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
]),
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.
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.
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).
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.
Every provider conforms to LLMProvider (a streaming chat(messages:tools:)).
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).JSProvider, backed by Resources/js/*.js over the native __http primitive): JSProviders.anthropic / .openai / .openaiCompatible / .ollama / .kimi / .google.EmbeddingProvider): HashingEmbeddingProvider (dependency-free, lexical) and OllamaEmbeddingProvider; MLX embeddings via KaozMLX.KaozMLX (opt-in) adds on-device Apple-silicon inference: MLXLLMProvider, MLXEmbeddingProvider, plus MLXModelStore / MLXDownloadCenter / ModelCatalogService for Hugging Face model management. Its Metal library isn't produced by swift build for a CLI, so run scripts/link-mlx-metallib.sh once after building to use --provider mlx from kaoz (see the script header).
Tools conform to Tool and register in a ToolRegistry. Read tools are safe by default; actuation is opt-in and confined.
ReadFileTool, ListDirectoryTool, GrepFilesTool (each confined to AuthorizedRoot folders), CurrentLocationTool, memory tools; JS tools current_datetime, fetch_url, web_search (Brave), news_search (NewsAPI).WriteFileTool / EditFileTool (confined to explicitly authorized write roots — a separate grant from read access), ShellTool (a fixed working directory), HTTPRequestTool (optional host allow-list).SendEmailTool / ReadEmailTool over SMTP/IMAP — any server, or a local Proton Bridge. send_email takes an optional html (sent as multipart/alternative with body as the text fallback) and attachments ({filename, contentType, base64, cid?}; a cid makes the file inline, referenced from the HTML as src="cid:NAME"). Recipients default to MAIL_TO and are confined to MAIL_ALLOWED_TO when set, so the model can't pick its own audience.HTTPPluginTool builds tools from a declarative PluginManifest + PluginSecrets — point it at any REST API with a JSON manifest, no code.SemanticMemoryStore (embedding-ranked recall) behind the MemoryStoring / MemoryRetrieving protocols.WebhookServer delivers inbound HTTP request bodies to a resident agent and replies with its result.kaozkaoz <agent.js> [flags] runs a standalone agent headless. Config (secrets, model) comes from the environment; the result is printed to stdout as JSON, errors to stderr with a non-zero exit.
| Flag | Effect |
|---|---|
--version | print the package version and the XS engine version it links |
--provider | anthropic · js-anthropic · js-openai · js-ollama · js-google · js-kimi · local · apple · mlx (default anthropic) |
--model M / --input JSON / --timeout SEC | model, agent input, per-run budget |
--library DIR / --modules nom=dir | extra module roots (the agent's own dir is always a root; resolution is confined) |
--root DIR | authorize a folder for the read file-tools |
--allow-write DIR / --allow-shell [--shell-dir DIR] / --allow-http [--http-host H] | opt-in actuation |
--email | enable send_email / read_email (SMTP/IMAP, see the env below) |
--persona FILE / --budget TOKENS | base identity prepended to every chat; hard token cap |
--resident [--daemon] [--state FILE [--state-auto]] | keep one engine alive; deliver a JSON message per stdin line; --daemon keeps scheduled ticks firing; --state snapshots the JS heap across processes (on exit, and whenever the agent calls host.snapshot()); --state-auto checkpoints after every delivery |
--webhook PORT | inbound HTTP → resident agent (implies resident + daemon) |
--embed-ollama MODEL | use a real embedding model for memory.search |
Secrets are read from the environment: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, MOONSHOT_API_KEY/KIMI_API_KEY, TYKAOZ_LOCAL_BASE_URL, BRAVE_API_KEY (enables web_search), TYKAOZ_MODEL, TYKAOZ_MEMORY_FILE.
--email reads its own set. Setting SMTP_HOST selects a real mail server (defaults: port 587, STARTTLS); leave it unset and the defaults target a local Proton Bridge under PROTON_BRIDGE_* instead. SMTP_* wins wherever both are defined.
| Variable | Role |
|---|---|
SMTP_HOST / SMTP_PORT / SMTP_TLS | server, port, and ssl | starttls | none |
SMTP_USER / SMTP_PASSWORD (or SMTP_PASS) | credentials |
SMTP_FROM (or EMAIL_FROM) | envelope sender (defaults to SMTP_USER) |
SMTP_IMAP_PORT / SMTP_IMAP_TLS | read_email side (defaults 993 / implicit TLS) |
MAIL_TO (or EMAIL_TO) | recipients used when the agent names none — a newsletter's audience is configuration, not a model's choice |
MAIL_ALLOWED_TO | comma-separated allowlist bounding what the agent may name; an @domain.tld entry allows a whole domain. Unset ⇒ unconfined |
PROTON_BRIDGE_HOST / _SMTP_PORT / _IMAP_PORT / _USER / _PASS / _FROM / _SMTP_TLS / _IMAP_TLS | the Proton Bridge equivalents |
The XS engine ships with a real source-level debugger, and kaoz is wired to it. This is a debugger for the agent's JavaScript — breakpoints, stepping, the call stack and every variable in scope — which no Node or Python agent harness gives you.
xsbug is Moddable's GUI debugger. It is built with the Moddable SDK tools (cd $MODDABLE/build/makefiles/mac && make) and lands in $MODDABLE/build/bin/mac/release/xsbug.app. Open it, then run kaoz as usual: every engine kaoz creates connects to xsbug on localhost:5002 automatically — there is no flag to pass. A debugger; statement anywhere in your agent stops execution right there, in xsbug, with the stack and the variables. Expect more than one machine in the list: the JS tool bundle, a JS provider, or a sub-agent each run in their own engine, and each is named (js-tools, the agent after its file, and so on).

xsdb is the command-line variant, modelled on gdb, and made to be driven by a script — or by an LLM: Claude Code can debug an agent with it. It lives in $MODDABLE/tools/xsbug-log (run npm install there once) and listens on the same port:
With the same debugger; as above — added to demo/hello.js right after the host.llm.chat call:
$ node $MODDABLE/tools/xsbug-log/xsbug-log.js # terminal 1 — waits for a connection
$ kaoz demo/hello.js --provider apple # terminal 2 — connects on its own
[Thread 1] Connected to "js-tools"
[Thread 2] Connected to "hello"
Debugger, run() at hello.js:13
(xsdb) bt
#0: run at hello.js:13
(xsdb) print question
question = 'What day is it today, and what can you do for me?'
(xsdb) continue
XSBUG_HOST and XSBUG_PORT redirect the connection (xsdb reads XSBUG_LOG_PORT for its own side) — handy when xsbug already holds 5002. JavaScript developers often reach for console.log(). Here is the alternative.
Under the agent runtime is a general-purpose JS↔Swift bridge, usable on its own. You expose native capabilities by writing a small C host-function target (against the classic xs.h API) that hands work to your Swift code; the engine runs on a private thread, Swift work runs off it, and await continuations resume across the boundary. This is exactly how KaozHostC + TyKaozHost.swift implement the agent's host.* surface — the demo pair KaozJSTestC/demoHost.c + KaozJSTests/DemoHost.swift (echo / stream / fail / add, plus a multi-machine service phase) is the reference pattern and doubles as the regression suite.
#include "xs.h"
#include "bridge.h"
#include "bridgeXS.h"
extern void myEcho(void* bridge, uint32_t id, const char* json); // @_cdecl in Swift
static void xs_echo(xsMachine* the) { // host.echo(x) — async
void* bridge = xsGetContext(the);
char* json = xsBridgeArgJSON(the, 0); // JSON.stringify(arg0), malloc'd
uint32_t id = xsServicePromise(the, NULL); // xsResult = the Promise
myEcho(bridge, id, json);
free(json);
}
import KaozJSCore // flat C settle functions
@_cdecl("myEcho")
func myEcho(_ bridge: UnsafeMutableRawPointer?, _ id: UInt32, _ json: UnsafePointer<CChar>?) {
guard let bridge else { return }
let payload = json.map { String(cString: $0) } ?? "null"
DispatchQueue.global().async {
xsServiceResolve(bridge, id, payload) // or xsServiceReject / xsServiceEmit
}
}
import KaozJS
import YourHostC
guard let engine = XSEngine() else { fatalError() }
engine.withMachine { MyHostInstall($0) } // install on the XS thread
let answer = try engine.eval("6 * 7") // "42"
try engine.runModule("agent.js") // runs export default
Core KaozJS API: XSEngine (eval, runModule, runUntilIdle, withMachine, installThreads, writeSnapshot, pendingCount; init(snapshot:)), XSCreation, XSError. The engine also snapshots its whole heap (persist/restore across launches), runs multi-machine services (JS-to-JS Thread / Service calls, values alien-marshalled), and confines module resolution to registered roots. See CLAUDE.md for the rooting rules, the snapshot callback-table, and the settle-path internals.
The XS engine sources are not vendored; they are linked from a local Moddable checkout (a recent master), so you need one before building. Three commands:
git clone --depth 1 https://github.com/Moddable-OpenSource/moddable.git
export MODDABLE="$PWD/moddable"
./scripts/link-moddable.sh # symlinks the XS subset into Sources/KaozJSCore/xs/
link-moddable.sh symlinks the curated subset the package compiles (xs/sources, xs/includes, the platform dispatch headers, and the macOS port mac_xs.c) and materializes an editable copy of mac_xs.h with the default module loaders turned off (the bridge supplies its own). Those links are git-ignored.
swift build -c release
swift run -c release KaozJSTests # engine regression suite; non-zero exit on any failure
swift run -c release kaoz demo/weather.js --provider anthropic --input '{"question":"…"}'
KaozJSTests is a multi-phase CLI harness whose demo host doubles as the engine regression suite; the JS fixtures it drives live in Sources/KaozJSTests/fixtures/, shipped as a resource of the target.
If a build hangs at 0 % CPU with no error, a stale macro-plugin binary in
.buildis the likely cause:KaozMLXexpands themlx-swift-lmmacros (#hubDownloader(),#huggingFaceTokenizerLoader()) through a plugin executable, and once that binary is left corrupt — by an interrupted build, say — every later compile reuses it and waits forever on a process that answers nothing. SwiftPM never rebuilds it on its own.rm -rf .buildclears it; re-runscripts/link-mlx-metallib.shafterwards, since the Metal library lives there too.
swift build -c release leaves the binary at .build/release/kaoz (on Apple Silicon, .build/arm64-apple-macosx/release/ is the same place). After that, kaoz and swift run -c release kaoz are equivalent; this README uses swift run in the Quick start and kaoz afterwards.
./scripts/install-kaoz.sh # symlinks /usr/local/bin/kaoz → .build/release/kaoz, then runs kaoz --version
Or without sudo: export PATH="$PWD/.build/release:$PATH".
It is a symlink, deliberately not a copy: the runtime's JavaScript resources and the MLX Metal library live next to the binary, and a copy moved elsewhere no longer finds them. The flip side: rm -rf .build breaks the link — run the script again after a clean build.
KaozKit is young and moving fast. The engine layer (KaozJS) is covered by a regression suite; the agent runtime API may still evolve before 1.0. Issues and questions are welcome — if you build something on KaozKit, I'd genuinely like to hear about it.
KaozKit is the foundation of TyKaoz — ty, the house, kaoz, the conversation, in Breton — a macOS app built in Rennes: a private AI wiki that answers questions from your own documents, with sources, entirely on-device. Coming soon; the library came first so the runtime would stand on its own.
Original project code is under the MIT License. It links against the Moddable XS engine, which is under the GNU LGPL v3 and is not redistributed here — you supply it via your own Moddable checkout. See NOTICE for details.
Practical implications:
KaozKit itself (MIT) imposes no restrictions either way.
97 commits
1 commits
Swift
78.3%
C
13.4%
JavaScript
7.5%
JavaScript LLM agents on the XS engine, embedded in Swift. Snapshots, resident agents, confined tools.
3
stars
98
commits
Swift
primary language
Sep 12, 2026
updated
Autonomous LLM agents, written in JavaScript, running inside your Swift app.
KaozKit embeds the XS engine (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.
// 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?"}'

(kaoz here is .build/release/kaoz on the PATH — see Install the kaoz CLI.)
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). Without them the tool is simply unavailable — an agent that asks for it gets a warning on stderr and carries on with the rest.
Fair question — JSC ships with the OS. XS earns its place with capabilities JSC doesn't have:
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.new Thread + new Service) as isolated XS machines with alien-marshalled calls between them.host.* surface is the only capability an agent has. Secrets never enter JS — providers are resolved and keys injected on the Swift side.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.
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
]),
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.
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.
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).
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.
Every provider conforms to LLMProvider (a streaming chat(messages:tools:)).
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).JSProvider, backed by Resources/js/*.js over the native __http primitive): JSProviders.anthropic / .openai / .openaiCompatible / .ollama / .kimi / .google.EmbeddingProvider): HashingEmbeddingProvider (dependency-free, lexical) and OllamaEmbeddingProvider; MLX embeddings via KaozMLX.KaozMLX (opt-in) adds on-device Apple-silicon inference: MLXLLMProvider, MLXEmbeddingProvider, plus MLXModelStore / MLXDownloadCenter / ModelCatalogService for Hugging Face model management. Its Metal library isn't produced by swift build for a CLI, so run scripts/link-mlx-metallib.sh once after building to use --provider mlx from kaoz (see the script header).
Tools conform to Tool and register in a ToolRegistry. Read tools are safe by default; actuation is opt-in and confined.
ReadFileTool, ListDirectoryTool, GrepFilesTool (each confined to AuthorizedRoot folders), CurrentLocationTool, memory tools; JS tools current_datetime, fetch_url, web_search (Brave), news_search (NewsAPI).WriteFileTool / EditFileTool (confined to explicitly authorized write roots — a separate grant from read access), ShellTool (a fixed working directory), HTTPRequestTool (optional host allow-list).SendEmailTool / ReadEmailTool over SMTP/IMAP — any server, or a local Proton Bridge. send_email takes an optional html (sent as multipart/alternative with body as the text fallback) and attachments ({filename, contentType, base64, cid?}; a cid makes the file inline, referenced from the HTML as src="cid:NAME"). Recipients default to MAIL_TO and are confined to MAIL_ALLOWED_TO when set, so the model can't pick its own audience.HTTPPluginTool builds tools from a declarative PluginManifest + PluginSecrets — point it at any REST API with a JSON manifest, no code.SemanticMemoryStore (embedding-ranked recall) behind the MemoryStoring / MemoryRetrieving protocols.WebhookServer delivers inbound HTTP request bodies to a resident agent and replies with its result.kaozkaoz <agent.js> [flags] runs a standalone agent headless. Config (secrets, model) comes from the environment; the result is printed to stdout as JSON, errors to stderr with a non-zero exit.
| Flag | Effect |
|---|---|
--version | print the package version and the XS engine version it links |
--provider | anthropic · js-anthropic · js-openai · js-ollama · js-google · js-kimi · local · apple · mlx (default anthropic) |
--model M / --input JSON / --timeout SEC | model, agent input, per-run budget |
--library DIR / --modules nom=dir | extra module roots (the agent's own dir is always a root; resolution is confined) |
--root DIR | authorize a folder for the read file-tools |
--allow-write DIR / --allow-shell [--shell-dir DIR] / --allow-http [--http-host H] | opt-in actuation |
--email | enable send_email / read_email (SMTP/IMAP, see the env below) |
--persona FILE / --budget TOKENS | base identity prepended to every chat; hard token cap |
--resident [--daemon] [--state FILE [--state-auto]] | keep one engine alive; deliver a JSON message per stdin line; --daemon keeps scheduled ticks firing; --state snapshots the JS heap across processes (on exit, and whenever the agent calls host.snapshot()); --state-auto checkpoints after every delivery |
--webhook PORT | inbound HTTP → resident agent (implies resident + daemon) |
--embed-ollama MODEL | use a real embedding model for memory.search |
Secrets are read from the environment: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, MOONSHOT_API_KEY/KIMI_API_KEY, TYKAOZ_LOCAL_BASE_URL, BRAVE_API_KEY (enables web_search), TYKAOZ_MODEL, TYKAOZ_MEMORY_FILE.
--email reads its own set. Setting SMTP_HOST selects a real mail server (defaults: port 587, STARTTLS); leave it unset and the defaults target a local Proton Bridge under PROTON_BRIDGE_* instead. SMTP_* wins wherever both are defined.
| Variable | Role |
|---|---|
SMTP_HOST / SMTP_PORT / SMTP_TLS | server, port, and ssl | starttls | none |
SMTP_USER / SMTP_PASSWORD (or SMTP_PASS) | credentials |
SMTP_FROM (or EMAIL_FROM) | envelope sender (defaults to SMTP_USER) |
SMTP_IMAP_PORT / SMTP_IMAP_TLS | read_email side (defaults 993 / implicit TLS) |
MAIL_TO (or EMAIL_TO) | recipients used when the agent names none — a newsletter's audience is configuration, not a model's choice |
MAIL_ALLOWED_TO | comma-separated allowlist bounding what the agent may name; an @domain.tld entry allows a whole domain. Unset ⇒ unconfined |
PROTON_BRIDGE_HOST / _SMTP_PORT / _IMAP_PORT / _USER / _PASS / _FROM / _SMTP_TLS / _IMAP_TLS | the Proton Bridge equivalents |
The XS engine ships with a real source-level debugger, and kaoz is wired to it. This is a debugger for the agent's JavaScript — breakpoints, stepping, the call stack and every variable in scope — which no Node or Python agent harness gives you.
xsbug is Moddable's GUI debugger. It is built with the Moddable SDK tools (cd $MODDABLE/build/makefiles/mac && make) and lands in $MODDABLE/build/bin/mac/release/xsbug.app. Open it, then run kaoz as usual: every engine kaoz creates connects to xsbug on localhost:5002 automatically — there is no flag to pass. A debugger; statement anywhere in your agent stops execution right there, in xsbug, with the stack and the variables. Expect more than one machine in the list: the JS tool bundle, a JS provider, or a sub-agent each run in their own engine, and each is named (js-tools, the agent after its file, and so on).

xsdb is the command-line variant, modelled on gdb, and made to be driven by a script — or by an LLM: Claude Code can debug an agent with it. It lives in $MODDABLE/tools/xsbug-log (run npm install there once) and listens on the same port:
With the same debugger; as above — added to demo/hello.js right after the host.llm.chat call:
$ node $MODDABLE/tools/xsbug-log/xsbug-log.js # terminal 1 — waits for a connection
$ kaoz demo/hello.js --provider apple # terminal 2 — connects on its own
[Thread 1] Connected to "js-tools"
[Thread 2] Connected to "hello"
Debugger, run() at hello.js:13
(xsdb) bt
#0: run at hello.js:13
(xsdb) print question
question = 'What day is it today, and what can you do for me?'
(xsdb) continue
XSBUG_HOST and XSBUG_PORT redirect the connection (xsdb reads XSBUG_LOG_PORT for its own side) — handy when xsbug already holds 5002. JavaScript developers often reach for console.log(). Here is the alternative.
Under the agent runtime is a general-purpose JS↔Swift bridge, usable on its own. You expose native capabilities by writing a small C host-function target (against the classic xs.h API) that hands work to your Swift code; the engine runs on a private thread, Swift work runs off it, and await continuations resume across the boundary. This is exactly how KaozHostC + TyKaozHost.swift implement the agent's host.* surface — the demo pair KaozJSTestC/demoHost.c + KaozJSTests/DemoHost.swift (echo / stream / fail / add, plus a multi-machine service phase) is the reference pattern and doubles as the regression suite.
#include "xs.h"
#include "bridge.h"
#include "bridgeXS.h"
extern void myEcho(void* bridge, uint32_t id, const char* json); // @_cdecl in Swift
static void xs_echo(xsMachine* the) { // host.echo(x) — async
void* bridge = xsGetContext(the);
char* json = xsBridgeArgJSON(the, 0); // JSON.stringify(arg0), malloc'd
uint32_t id = xsServicePromise(the, NULL); // xsResult = the Promise
myEcho(bridge, id, json);
free(json);
}
import KaozJSCore // flat C settle functions
@_cdecl("myEcho")
func myEcho(_ bridge: UnsafeMutableRawPointer?, _ id: UInt32, _ json: UnsafePointer<CChar>?) {
guard let bridge else { return }
let payload = json.map { String(cString: $0) } ?? "null"
DispatchQueue.global().async {
xsServiceResolve(bridge, id, payload) // or xsServiceReject / xsServiceEmit
}
}
import KaozJS
import YourHostC
guard let engine = XSEngine() else { fatalError() }
engine.withMachine { MyHostInstall($0) } // install on the XS thread
let answer = try engine.eval("6 * 7") // "42"
try engine.runModule("agent.js") // runs export default
Core KaozJS API: XSEngine (eval, runModule, runUntilIdle, withMachine, installThreads, writeSnapshot, pendingCount; init(snapshot:)), XSCreation, XSError. The engine also snapshots its whole heap (persist/restore across launches), runs multi-machine services (JS-to-JS Thread / Service calls, values alien-marshalled), and confines module resolution to registered roots. See CLAUDE.md for the rooting rules, the snapshot callback-table, and the settle-path internals.
The XS engine sources are not vendored; they are linked from a local Moddable checkout (a recent master), so you need one before building. Three commands:
git clone --depth 1 https://github.com/Moddable-OpenSource/moddable.git
export MODDABLE="$PWD/moddable"
./scripts/link-moddable.sh # symlinks the XS subset into Sources/KaozJSCore/xs/
link-moddable.sh symlinks the curated subset the package compiles (xs/sources, xs/includes, the platform dispatch headers, and the macOS port mac_xs.c) and materializes an editable copy of mac_xs.h with the default module loaders turned off (the bridge supplies its own). Those links are git-ignored.
swift build -c release
swift run -c release KaozJSTests # engine regression suite; non-zero exit on any failure
swift run -c release kaoz demo/weather.js --provider anthropic --input '{"question":"…"}'
KaozJSTests is a multi-phase CLI harness whose demo host doubles as the engine regression suite; the JS fixtures it drives live in Sources/KaozJSTests/fixtures/, shipped as a resource of the target.
If a build hangs at 0 % CPU with no error, a stale macro-plugin binary in
.buildis the likely cause:KaozMLXexpands themlx-swift-lmmacros (#hubDownloader(),#huggingFaceTokenizerLoader()) through a plugin executable, and once that binary is left corrupt — by an interrupted build, say — every later compile reuses it and waits forever on a process that answers nothing. SwiftPM never rebuilds it on its own.rm -rf .buildclears it; re-runscripts/link-mlx-metallib.shafterwards, since the Metal library lives there too.
swift build -c release leaves the binary at .build/release/kaoz (on Apple Silicon, .build/arm64-apple-macosx/release/ is the same place). After that, kaoz and swift run -c release kaoz are equivalent; this README uses swift run in the Quick start and kaoz afterwards.
./scripts/install-kaoz.sh # symlinks /usr/local/bin/kaoz → .build/release/kaoz, then runs kaoz --version
Or without sudo: export PATH="$PWD/.build/release:$PATH".
It is a symlink, deliberately not a copy: the runtime's JavaScript resources and the MLX Metal library live next to the binary, and a copy moved elsewhere no longer finds them. The flip side: rm -rf .build breaks the link — run the script again after a clean build.
KaozKit is young and moving fast. The engine layer (KaozJS) is covered by a regression suite; the agent runtime API may still evolve before 1.0. Issues and questions are welcome — if you build something on KaozKit, I'd genuinely like to hear about it.
KaozKit is the foundation of TyKaoz — ty, the house, kaoz, the conversation, in Breton — a macOS app built in Rennes: a private AI wiki that answers questions from your own documents, with sources, entirely on-device. Coming soon; the library came first so the runtime would stand on its own.
Original project code is under the MIT License. It links against the Moddable XS engine, which is under the GNU LGPL v3 and is not redistributed here — you supply it via your own Moddable checkout. See NOTICE for details.
Practical implications:
KaozKit itself (MIT) imposes no restrictions either way.
97 commits
1 commits
Swift
78.3%
C
13.4%
JavaScript
7.5%