← Articles EN
KaozKit · XS · Swift

Faire entrer un moteur JavaScript vieux de vingt ans dans l'ère des agents IA

Comment j'ai construit KaozKit — des agents LLM écrits en JavaScript, exécutés dans une app Swift, dont l'état entier survit à un redémarrage parce que leur cerveau est un snapshot de heap.

Sébastien Burel · haruni.net · Août 2026

Il y a vingt ans, j'étais associate chez Kinoma, en Californie. Nous construisions des logiciels pour de tout petits appareils, et au cœur de tout ça se trouvait un moteur JavaScript conçu pour tourner là où rien d'autre ne tenait : XS. Il était petit, strict et élégant — l'œuvre de Patrick Soquet, qui en est toujours l'architecte aujourd'hui.

Kinoma n'existe plus, mais XS, si. Il vit chez Moddable, l'entreprise fondée par Peter Hoddie et Patrick, où il fait tourner des objets connectés partout dans le monde. Je m'en suis servi une fois de plus entre-temps, pour sortir Frigo Magic, une app grand public écrite entièrement en XS. Puis je suis passé à autre chose, comme on fait.

L'hiver dernier, j'ai commencé à construire une app Mac — un wiki IA privé, où vous posez des questions sur vos propres documents et où tout reste sur votre machine. Très vite, je suis tombé sur la question que rencontre toute app d'IA : qu'est-ce qui exécute les agents ? Pas le modèle — l'agent : le petit programme qui décide quel outil appeler, conserve son état d'un tour à l'autre, se souvient de ce qu'il a appris, et se réveille à l'heure dite pour recommencer.

Les réponses existantes se ressemblaient toutes. OpenClaw exige Node. Hermes exige Python. Très bien pour un serveur ; inadapté à une app Mac native qui promet que « rien ne quitte votre machine » et qui ne peut décemment pas embarquer un runtime de 100 Mo pour exécuter un script de dix lignes.

Puis je me suis souvenu de XS.


Un agent est un module

Voici un agent KaozKit complet :

// agent.js — s'exécute dans le moteur
export async function run(input) {
  const reply = await host.llm.chat(
    [{ role: "user", content: input.question }],
    { tools: ["current_datetime", "web_search"] }   // le modèle peut les appeler
  );
  await host.memory.save("last question", input.question);
  return { answer: reply };
}

C'est tout. Un module JS exporte run(input). Il parle à un modèle de langage via host.llm.chat, qui exécute toute la boucle d'appel d'outils en interne — le modèle demande un outil, Swift l'exécute, le modèle continue — et se résout avec le texte final. Il lit et écrit en mémoire. Il renvoie du JSON à l'hôte.

On le lance depuis la ligne de commande :

kaoz agent.js --provider apple --input '{"question":"what day is it?"}'

Avec --provider apple, ça tourne sur Apple Intelligence — entièrement en local sur l'appareil, sans clé d'API, sans réseau. Remplacez par --provider anthropic ou --provider mlx et l'agent ne change pas ; c'est l'hôte qui change.

Le global host constitue toute la surface de capacités d'un agent. Il n'y a pas de fetch, pas de système de fichiers, pas de require. Un agent ne peut faire que ce que l'hôte expose explicitement : host.llm, host.provider(id), host.tool.call, host.memory, host.schedule. Les clés d'API sont résolues côté Swift et n'entrent jamais dans le JavaScript. La résolution de modules est confinée à des racines enregistrées. Si vous avez déjà passé du temps à réfléchir à ce qu'un script non fiable devrait avoir le droit de faire, c'est la partie qui vous permet de dormir.


Pourquoi XS, et pas JavaScriptCore ?

Bonne question — JSC est livré avec tous les Mac. J'ai commencé avec. J'en ai changé pour quatre raisons, dont chacune s'est révélée être une fonctionnalité produit plutôt qu'un détail d'implémentation.

Les snapshots de heap. XS sait sérialiser l'intégralité de son heap en octets et le restaurer dans un nouveau processus. Pas « sérialisez votre état en JSON » — le heap : chaque objet, chaque closure, chaque timer en attente, la conversation au milieu de laquelle se trouve un agent. Dans KaozKit, ce sont writeSnapshot() / init(snapshot:). Il m'a fallu un moment pour en mesurer la portée : le cerveau d'un agent résident est un fichier. Tuez le processus, copiez le fichier sur une autre machine, restaurez — l'agent reprend en pleine pensée. Je n'ai écrit aucune ligne de code de sérialisation, et je n'en écrirai jamais.

Une machine par agent. XS a été conçu pour les microcontrôleurs. Une machine coûte très peu de mémoire, ce qui fait de « un moteur isolé par agent » une architecture raisonnable plutôt qu'un luxe. Un orchestrateur peut lancer des sous-agents comme autant de machines XS distinctes (new Thread + new Service, avec les appels marshallés à travers la frontière) et garder plusieurs agents résidents vivants côte à côte. Avec JSC, j'aurais passé mon temps à compter les contextes.

Le confinement par construction. XS n'a aucune autorité ambiante. Il n'y a rien à mettre hors du bac à sable — on ajoute des capacités, une fonction hôte à la fois, en C, contre la classique API xs.h. Le côté Swift les implémente ; le côté C est une fine couche qui transmet le travail et résout les promesses quand Swift a terminé. C'est exactement la discipline qu'emploient les développeurs embarqué pour exposer un capteur à un script, appliquée à l'exposition d'un modèle de langage.

Un pont asynchrone propre. Le moteur tourne sur un thread privé ; le travail Swift s'exécute en dehors ; les continuations d'await se résolvent correctement à travers la frontière. C'était la partie difficile, et c'est là que l'aide de Patrick a été décisive — une partie de la mécanique qui rend tout ça propre se trouve aujourd'hui dans la couche C de KaozKit, et une autre partie relève simplement de savoir quels invariants XS garantit et lesquels il ne garantit pas.

Si vous avez seulement besoin d'évaluer un script, JSC fait très bien l'affaire. Si vous voulez des agents à état, redémarrables et confinés, c'est à ça que sert KaozKit.


Ce que le snapshot change en pratique

La première chose que j'ai construite une fois les snapshots opérationnels, c'est un agent résident à qui je pouvais parler, que je pouvais tuer, puis à qui je pouvais reparler :

kaoz concierge.js --resident --daemon --state brain.bin --provider anthropic

Vous lui envoyez des lignes JSON sur stdin. Il se souvient de ce que vous avez dit. Vous lui demandez de vous rappeler quelque chose dans vingt minutes — ça arme un timer via host.schedule. Puis vous faites Ctrl-C, vous regardez brain.bin, et vous le redémarrez avec la même commande. Il répond correctement à « de quoi parlions-nous ? », et vingt minutes après la demande initiale, le rappel se déclenche — dans un processus qui n'existait pas au moment où il a été programmé.

J'ai montré ça à Peter et Patrick il y a quelques semaines. Il y a un plaisir particulier à démontrer aux auteurs d'un moteur une chose que leur moteur sait faire et qu'ils n'avaient jamais eu de raison d'essayer.

La deuxième chose que j'ai construite est un véritable framework d'acteurs par-dessus — les primitives d'Agha, create / send / become, avec une petite couche d'agents LLM, en quelques centaines de lignes de JavaScript qui s'exécutent intégralement dans XS (les tests compris). L'invariant qui compte, ici, est que les handlers de messages sont synchrones : un seul await au milieu d'un handler, et le message suivant s'entrelace avec un état à moitié muté. Tout ce qui est asynchrone — un appel LLM, un outil — entre et sort par message. Et comme les comportements et les mailboxes vivent dans le heap, le snapshot persiste le système d'acteurs tout entier : un pipeline à mi-chemin de la collecte sur trente sources survit à un kill -9 et reprend.

Ce framework fait aujourd'hui tourner un agent de newsletter quotidienne sur mon Mac et, depuis cette semaine, l'agent qui prépare mes lectures du matin. Les deux ont été construits selon la même règle : pas de Node nulle part, tout passe par kaoz.


Les couches

KaozKit est un unique package SwiftPM qui expose ses produits en couches, pour que vous ne preniez que ce dont vous avez besoin :

KaozJSCore (C)  — moteur XS + pont de résolution asynchrone
KaozJS          — XSEngine Swift : thread dédié, snapshot, racines de modules
KaozHostC (C)   — les fonctions hôtes de l'agent (host.llm / tool / memory / schedule)
KaozKit         — runtime d'agents : providers, outils, mémoire, canaux, persona
KaozMLX         — inférence locale MLX (dépendances lourdes, optionnel)
kaoz            — CLI headless / daemon résident

Si vous voulez seulement un moteur JS↔Swift avec des snapshots, KaozJS s'utilise seul. Si vous voulez des agents, KaozKit tire le reste. Les providers couvrent Anthropic, OpenAI, Google, Mistral, DeepSeek, Ollama, LM Studio, Apple Intelligence, MLX, et ComfyUI pour les images — derrière un unique protocole LLMProvider, ce qui permet à un agent de les chaîner : demander à Claude d'écrire un prompt d'image, le passer à ComfyUI, renvoyer l'image.

Les outils suivent la même philosophie : les outils de lecture sont sûrs par défaut et confinés à des dossiers autorisés ; tout ce qui agit — écrire des fichiers, lancer une commande shell, faire une requête HTTP — est opt-in, à chaque invocation, avec sa propre liste blanche. Et il existe un format de plugin déclaratif : pointez un manifeste JSON vers n'importe quelle API REST, et elle devient un outil que le modèle peut appeler.


La licence, franchement

KaozKit est sous licence MIT. Le moteur XS auquel il se lie est celui de Moddable, sous LGPL v3, et il n'est pas vendorisé : vous le fournissez depuis votre propre checkout Moddable, et un script crée des liens symboliques vers le sous-ensemble que le package compile. Pour un usage open source ou non distribué, il n'y a rien de plus à faire.

Si vous distribuez un binaire propriétaire — en particulier sur le Mac App Store, où l'obligation de relinking de la LGPL n'est pas réalistement satisfaisable — il vous faut une licence XS commerciale auprès de Moddable. Ils en proposent exactement pour ce cas ; ma propre app est licenciée ainsi. Je le mentionne d'emblée parce que c'est la première question que je poserais.


La suite

KaozKit est le socle de TyKaozty, la maison, kaoz, la conversation, en breton — le wiki IA privé dont je parlais au début, construit à Rennes, lancement cet automne sur macOS. La bibliothèque est sortie en premier, volontairement : le runtime devait tenir debout tout seul avant qu'on bâtisse la maison dessus.

Le code est sur github.com/sebastien-burel/KaozKit — macOS 26+, Apple Silicon, MIT. C'est jeune, et l'API des agents peut encore bouger avant la 1.0. Si vous avez embarqué XS, construit un runtime d'agents, ou si vous voulez juste débattre des snapshots de heap, ça m'intéresse sincèrement : demat@tykaoz.bzh, ou une issue sur le dépôt.

Et si vous étiez chez Kinoma — vous vous reconnaîtrez — celui-ci est une réunion d'anciens.

Sébastien Burel construit TyKaoz chez Haruni, à Rennes. Il a été associate chez Kinoma et a sorti Frigo Magic entièrement en XS.

Un projet similaire vous intéresse ? Je suis disponible pour des missions de machine learning engineering — déploiement de LLM, optimisation d'inférence, évaluation de modèles. N'hésitez pas à me contacter.