L'interface est une application React rendue par [Ink](https://github.com/vadimdemedes/ink) (Ink 7 : React ≥ 19.2 ; vérifiez la version de Node qu'il exige).
Le CLI garde une façade impérative (`InkUI` : `info`, `select`, `readInput`, `confirm`, `withSpinner`…) au-dessus d'un `UIStore` sans dépendance à React,
ce qui rend la logique testable (`store.test.ts`). Les composants (saisie, liste, confirmation, spinner) sont écrits à la main ; seuls les badges viennent de [`@inkjs/ui`](https://github.com/vadimdemedes/ink-ui).
Présentation : la bannière d'accueil reprend le style « MAIA » : image à gauche, affichée telle quelle en couleurs réelles (demi-blocs ▀▄█ : 32 × 32 pixels, soit 32 colonnes × 16 lignes ; sans pixelisation). Elle vient de `packages/ui/assets/mask.png` ; `python3 packages/ui/scripts/make-mask.py [taille]` (Pillow) régénère `src/mask-data.ts` à une autre taille, titre « Maia » en gros caractères pixel avec ombre et dégradé arc-en-ciel à droite, puis deux textes (vert : « Assistant IA en ligne de commande… », rose : « Un seul agent, plusieurs modèles »). Terminal d'au moins 58 colonnes pour la version complète ; entre 25 et 57 : sans le masque ; en dessous : cadre à double bordure compact, titre centré ;
Les réponses de l'agent sont rendues en markdown (titres, listes, code, tableaux, gras…) avec [`markdansi`](https://github.com/steipete/markdansi). Tout est isolé dans `packages/ui/src/markdown.tsx` : pour le retirer, supprimer ce fichier, la dépendance `markdansi` et remplacer `<Markdown />` par `<Text>` dans `components.tsx`. Le texte en cours de streaming reste brut ; la mise en forme apparaît quand la réponse est terminée. `MAIA_NO_MARKDOWN=1` désactive le rendu sans toucher au code, et un échec de `markdansi` retombe sur le texte brut.
Les couleurs sont dans `packages/ui/src/colors.ts`. Une fois la réponse validée, la question est remplacée par la ligne `◆ Question : choix` dans le journal.
Barre de contexte : sous la zone de saisie, en bas à droite, une barre de carrés ■ (de gauche à droite) montre la taille du contexte de la conversation par rapport à la taille maximale du modèle, à l'échelle **1 carré = 10 000 tokens** (200 000 tokens → 20 carrés), avec le libellé « 45.7k / 200k · 23% ». Les carrés sont verts, jaunes à partir de 60 %, rouges à partir de 85 %. Si la barre ne tient pas dans le terminal (très grands contextes), l'échelle est agrandie par paliers de 10 000 et indiquée (`1■ = 20k`).
- **Taille maximale** : après le choix du modèle (au démarrage, avec `--model`/config, et avec `/model`), MAIA demande au LLM « Taille maximale du contexte pour ce modèle » (réponse en tokens, lue par `parseTokenCount` : `128000`, `128 000 tokens`, `200k`, `1,5 million`…). La valeur est annoncée (INFO) ; une réponse par modèle et par exécution. Si la réponse est illisible, ou si la question échoue ou dépasse 60 s (Ctrl+C l'abandonne), un avertissement s'affiche et la barre est masquée. Un LLM peut se tromper sur sa propre limite : la valeur affichée en INFO permet de la vérifier.
- **Contexte utilisé** = tokens de tout ce que la session enverrait au modèle : prompt système (MAIA.md…), outils, messages, et contexte en attente (résultats de commandes `!`, ajoutés à la barre dès leur exécution). Après chaque réponse du LLM (donc aussi après chaque appel d'outil intermédiaire), la valeur est celle renvoyée par LiteLLM (`stream_options.include_usage`) : tokens de la requête + tokens de la réponse conservés dans la conversation (les tokens de raisonnement, non renvoyés au tour suivant, sont exclus). Entre deux réponses (démarrage, `/clear`, `/new`, `/load`, tour interrompu, commande `!`), la valeur est une estimation, signalée par un `~` devant le nombre ; elle est calibrée à chaque réponse sur le rapport réel/estimé mesuré par le serveur.
La zone de saisie est encadrée par une bordure en pointillés (┈) **au-dessus et au-dessous** seulement, en `greenBright`, sans bords gauche ni droit.
À sa gauche, le spinner `arrow3` de [cli-spinners](https://github.com/sindresorhus/cli-spinners) s'anime en continu. Pendant l'attente de la réponse après validation de la saisie, « En réflexion » s'affiche avec le spinner `dots10` à sa gauche, en `yellowBright` (même spinner pour les autres attentes : chargement des modèles, exécution d'une commande `!`).
(`~/…` désigne le dossier personnel). Ce dossier est capturé une fois au démarrage, affiché (« Dossier de travail : … ») et transmis aux outils et aux providers ;
les règles de permissions (`Read(./.env)`, `Edit(src/**)`) sont elles aussi relatives à ce dossier. Seul `~/.maia` (config, `settings.json` et skills utilisateur) est lu depuis le dossier personnel.
- stdout et stderr sont affichés dans l'ordre d'arrivée, avec le code de sortie en cas d'échec ; durée maximale 2 minutes ;
- aucune confirmation n'est demandée (c'est vous qui la tapez) et les règles de permissions des outils ne s'appliquent pas ;
- le résultat est ajouté au contexte de **votre prochaine demande** (jusqu'à 30 000 caractères, milieu tronqué au-delà), pour que le modèle sache ce qui a été lancé. `/clear` l'efface ;
- Ctrl+C interrompt la commande ; `!` seul affiche l'aide. Les programmes interactifs (vim, less, saisie au clavier…) ne sont pas pris en charge.
| `compare @a.ts et @ b.ts` | plusieurs fichiers ; la forme `@ chemin` (avec espace) est acceptée |
| `lis @'mon dossier/a b.txt'` | guillemets simples pour un chemin avec espaces (`\'` : guillemet littéral) |
| `résume @docs` | un dossier : la liste de ses entrées est injectée |
| `@/chemin/complet.txt` · `@~/notes.md` | chemin complet, ou depuis le dossier personnel |
- Chemins relatifs au dossier de lancement (voir « Dossier de base »). Aucune confirmation : c'est vous qui citez le fichier, les règles de permissions des outils ne s'appliquent pas.
- Un `@mot` qui ne désigne aucun fichier existant reste du texte ordinaire (`@mention`, `@Override`, `jean@exemple.fr`) ; un avertissement n'apparaît que s'il ressemble à un chemin (`@src/x.ts` introuvable).
- Limites : 2 Mo par fichier, 50 000 caractères injectés par fichier (milieu tronqué au-delà) et 150 000 par message ; les fichiers binaires sont ignorés ; un même fichier cité plusieurs fois n'est injecté qu'une fois.
- Le contenu est marqué comme donnée, pas comme instructions. Les `@fichier` écrits *dans* le corps d'une commande personnalisée ne sont pas encore traités (seuls ceux de votre saisie le sont).
Au lancement, `./.maia` et `./.maia/.sessions` sont créés dans le dossier d'exécution s'ils n'existent pas. Les sessions sont une fonctionnalité de l'application.
-`/new` : nouvelle session, nouveau contexte (`MAIA.md` / `CLAUDE.md` et skills rechargés) ; l'utilisateur est averti que la session quittée est sauvegardée (sous son nom).
-`OpenAI_EndPoint` : suffixe ajouté à `LiteLLM_URL` pour envoyer une requête au format OpenAI.
-`LiteLLM_Models` : suffixe ajouté à `LiteLLM_URL` pour récupérer la liste des modèles disponibles.
- Optionnel : `LiteLLM_API_Key` (ou variable d'environnement `LITELLM_API_KEY`), envoyée en `Authorization: Bearer …`. Ce fichier peut donc contenir un secret : ne pas le versionner.
Au lancement, le contexte de départ est chargé depuis le **dossier de lancement** : `MAIA.md` s'il existe, sinon `CLAUDE.md` (s'il existe).
Le fichier chargé est indiqué par un message `INFO` ; si aucun des deux n'existe, un `WARN` le signale. Seul ce dossier est regardé (pas les dossiers parents).
Les dossiers `.claude` ne sont **plus lus** (ni `settings.json`, ni `commands/`, ni `skills/`, ni `.claude/CLAUDE.md`, ni `~/.claude`).
- **Skills** : `.maia/skills/<nom_du_skill>/SKILL.md` (ou `skill.md`), chargés depuis trois emplacements. Le plus spécifique l'emporte en cas de même nom :
1. le dossier utilisateur : `~/.maia/skills` ;
2. le dossier parent du dossier de lancement : ex. `/X/.maia/skills` pour un lancement dans `/X/Y` ;
3. le dossier de lancement : ex. `/X/Y/.maia/skills`.
Le fichier commence par un en-tête YAML : `description` (objectif du skill) et `name` (optionnel ; s'il est présent, il doit être identique au nom du dossier, qui fait foi).
Seule la liste (nom + description) est mise dans le prompt ; le contenu est chargé à la demande par l'outil `Skill`, ou lancé directement avec `§nom`.
```
---
name: sk_test # optionnel, = nom du dossier
description: Lance les tests du projet et résume les échecs
---
Instructions du skill… ($ARGUMENTS = arguments passés avec §sk_test)
Conséquence : il n'y a plus de variables d'environnement venant de la configuration de l'agent ni de slash-commandes personnalisées. Les permissions sont décrites dans la section suivante.
## Permissions
Les permissions s'appliquent aux **outils** (intégrés ou non). Trois types de règles :
| Type | Effet |
| --- | --- |
| `allow` | l'outil est utilisé sans approbation manuelle |
| `ask` | l'utilisateur doit confirmer chaque utilisation (badge `ASK`, réponse `o`/`n`) |
| `deny` | l'outil ne peut pas être utilisé (l'agent reçoit un refus) |
**Ordre d'évaluation : deny, puis ask, puis allow.** La première famille qui correspond décide, quelle que soit la précision des règles : un `allow` étroit comme `Bash(aws s3 ls)` ne fait pas exception à un `deny` large comme `Bash(aws *)`, ni à un `ask` plus large. **Sans règle applicable, c'est `ask` qui s'applique** (pour tous les outils, y compris Read, Glob, Grep…).
Où les définir : `~/.maia/settings.json` (utilisateur) et `./.maia/settings.json` (dossier de lancement). Les listes des deux fichiers s'additionnent. Le fichier `../.maia/settings.json` du dossier parent n'est **pas** pris en compte. Dans chaque fichier, la propriété `permissions` contient une liste par type, tous facultatifs :
Format d'une règle : `Outil` (tous les appels de l'outil) ou `Outil(motif)` :
| Outil | Le motif porte sur | Exemples |
| --- | --- | --- |
| `Bash` | la commande ; `*` = n'importe quelle suite ; `cmd *` couvre `cmd` avec ou sans arguments (mais pas `cmdx`) ; sans `*` : commande exacte | `Bash(allow stop *)`, `Bash(git status)`, `Bash(npm run test:*)` |
| `Read`, `Write`, `Edit`, `Glob`, `Grep` | le chemin (relatif au dossier de lancement, motifs glob) | `Read(./.deny.md)`, `Edit(src/**)` |
| `WebFetch` | le domaine | `WebFetch(domain:docs.example.com)`, `WebFetch(domain:*.example.org)` |
| `Skill` | le nom du skill | `Skill(pdf)` |
| autres outils (ex. ajoutés plus tard) | une valeur texte de l'entrée de l'outil (`*` accepté) | `mcp__db__query(SELECT *)` |
Précisions sur `Bash` :
- commande composée (`&&`, `;`, `|`) : un `deny` ou un `ask` s'applique dès qu'**une** des parties correspond, y compris cachée dans `$(…)`, des backticks ou des parenthèses ; un `allow` ne s'applique que si **toutes** les parties sont couvertes ;
- un `allow` ne couvre jamais une commande avec redirection (`>`) ou substitution (`$(…)`, backticks) : elle repasse en `ask`.
Au démarrage, MAIA indique (`INFO`) les fichiers chargés et le nombre de règles, ou qu'aucune règle n'est définie ; un JSON invalide ou une règle mal formée donne un `WARN` et est ignoré. Les `deny` sont du « meilleur effort » : ils ne remplacent pas un vrai bac à sable. Les commandes `!` et les références `@fichier` tapées par l'utilisateur ne passent pas par ces règles.
Attention : `./.maia/settings.json` d'un dépôt tiers peut contenir des règles `allow` larges ; relisez-le avant de lancer MAIA dans un projet que vous ne connaissez pas.
La commande `/permissions` affiche les règles actives, fichier par fichier, avec un badge `ALLOW` (vert), `ASK` (jaune) et `DENY` (rouge) suivi des règles du type, une par ligne ; seuls les types non vides sont montrés. Après avoir modifié un `settings.json`, `/clear` recharge la configuration.
- **TodoWrite** : liste de tâches de la session (`pending` / `in_progress` / `completed`, forme `activeForm` pour la tâche en cours). Chaque appel remplace la liste ; le résultat (☐ ▶ ☑) est affiché à l'utilisateur et renvoyé au modèle. Un avertissement est ajouté si plusieurs tâches sont « en cours ».
- **CurrentModel** : renvoie le modèle LLM utilisé par la session (valeur à jour après un `/model`). Sans paramètre, sans confirmation ; le modèle est lu au moment de l'appel.
- **WebFetch** : télécharge une URL http(s) (HTML converti en Markdown avec `turndown`, JSON mis en forme, texte brut ; 5 Mo et 30 s maximum, `max_length` caractères renvoyés). Pas de JavaScript exécuté, pas de PDF ni d'images.
- Une redirection vers un **autre domaine** n'est pas suivie : l'outil renvoie la nouvelle URL, que le modèle doit rappeler (ce qui repasse par la confirmation).
- Le contenu est signalé au modèle comme non fiable (risque d'injection de prompt) ; il ne passe par aucun second modèle de résumé.
L'utilisateur peut créer ses propres outils programmatiques : le LLM les appelle comme n'importe quel autre outil quand leur description correspond au besoin. Ils sont chargés au démarrage et rechargés par `/clear` et `/new`.
- **Où** : uniquement dans `./.maia/tools` du dossier de lancement (jamais le dossier parent ni le dossier de l'utilisateur). Un sous-dossier = un outil : `./.maia/tools/addition/TOOL.json` (`tool.json` accepté aussi) et ses scripts. Un exemple complet est fourni dans `examples/tools/addition` (à copier dans `./.maia/tools/`).
- **`TOOL.json`** :
```json
{
"name": "Addition",
"description": "Faire une addition avec 3 chiffres. Renvoie un chiffre, la somme",
| `description` | **Obligatoire** : dit au LLM à quoi sert l'outil et quand l'appeler. |
| `name` | Facultatif, le nom du dossier par défaut. Lettres, chiffres, `_` et `-` (64 caractères maximum). Ne peut pas reprendre le nom d'un outil intégré (Read, Bash…) ni d'un autre outil personnalisé. |
| `command.parameters` | Paramètres fournis par le LLM. `type` : `"number"` ou `"string"`. `default` : valeur utilisée si le LLM n'en donne pas (ex. `{{param2}}` devient `0`) ; sans `default`, le paramètre est obligatoire. `description` facultative par paramètre. |
| `command.run` | Ligne de commande ; chaque `{{paramètre}}` est remplacé par sa valeur. `./` désigne le dossier de l'outil (`./test.js` = `./.maia/tools/addition/test.js`) et c'est le dossier d'exécution. |
- **Sécurité : pas de shell.** La ligne `run` est découpée en arguments (guillemets simples ou doubles possibles) et exécutée directement : une valeur envoyée par le LLM reste **toujours un seul argument** (`; rm -rf /` ou `$(cmd)` sont reçus tels quels par le script, jamais interprétés). En contrepartie, `run` n'accepte ni tubes `|`, ni redirections `>`, ni variables `$HOME` ; pour cela, appeler un script (`sh ./script.sh {{p}}`). Le programme (premier mot) ne peut pas être un `{{paramètre}}`. Sous Windows, les scripts `.cmd`/`.bat` ne sont pas lancés directement (utiliser `node`, `python`, ou `cmd /c ./script.cmd`).
- **Exécution** : timeout de 2 minutes, sortie limitée à 30 000 caractères ; un code de sortie non nul est renvoyé au LLM comme une erreur (avec la sortie d'erreur du script). Le script reçoit `MAIA_CWD` (dossier de lancement de MAIA, pour retrouver le projet) et `MAIA_TOOL_DIR`.
- **Permissions** : un outil personnalisé exécute un programme, donc il demande confirmation par défaut. Les règles de permissions s'appliquent avec son nom : `"Addition"` dans `allow` l'autorise sans demander, `"Addition(5)"` cible une valeur de paramètre.
- **Erreurs** : une définition invalide (description absente, type inconnu, `{{param}}` non déclaré, JSON cassé…) est signalée par un avertissement (`WARN`) et l'outil est ignoré ; les autres restent chargés. Une virgule en trop avant `]` ou `}` est tolérée. Au chargement, les outils valides sont annoncés (`INFO`).
## Exécution des outils : premier plan et parallèle
- Le **raisonnement du LLM reste bloquant** : l'utilisateur attend la réponse, ce qui préserve l'intégrité du contexte.
- **1 outil demandé** : exécuté au premier plan (annonce, confirmation éventuelle, exécution, résultat).
- **2 outils ou plus** dans la même réponse : chaque outil part dans sa **propre tâche, en arrière-plan**, dès qu'il est autorisé. L'agent attend la fin de **toutes** les tâches, puis renvoie les résultats au LLM **dans l'ordre où il les a demandés**. Une erreur ou un refus n'annule pas les autres outils.
- Le spinner indique « Outils en parallèle : n/total terminés (…) ».
- Limites : ce sont des tâches asynchrones Node (pas des threads système ; les processus lancés par Bash ou les outils personnalisés tournent réellement en parallèle) ; 8 outils au plus en même temps (`MAX_PARALLEL_TOOLS`) ; les confirmations sont posées une par une ; les appels qui portent sur le même fichier (Read/Write/Edit) ou TodoWrite passent l'un après l'autre (`Tool.lockKey`).
Le compactage résume toute la conversation de la session pour libérer du contexte.
1. La conversation actuelle est sauvegardée telle quelle sous son nom.
2. Le LLM reçoit la demande « Faire un résumé compacté de nos échanges… » (5 rubriques : Contexte du projet, Objectif actuel, Fichiers pertinents, Décisions/Conclusions, Prochaines étapes), limité à 4000 tokens. Les commandes `!` et fichiers `@` pas encore envoyés font partie de ce qui est résumé.
3. Le contexte est supprimé comme avec `/clear`, mais `MAIA.md` / `CLAUDE.md` ne sont **pas** rechargés : le résumé les remplace comme contexte de départ (dans le prompt système). Outils, skills et permissions sont rechargés.
4. La session continue sous le nom `{nom d'origine}__{n}` : `xxx` → `xxx__1` → `xxx__2`… Après `__1000`, le suivant reprend à `__1` et écrase ce fichier. Le fichier d'avant compactage reste intact ; le fichier `xxx__n` contient le résumé (`startContext`) et la suite de la conversation, et `/load xxx__n` le restaure.
5. Le nombre de tokens est recalculé et la barre de progression mise à jour.
Déclenchement automatique : dès que le contexte dépasse `Seuil_max = Max_Token_Context - (4000 + 500)` (4000 = tokens du résumé, 500 = marge d'erreur). La taille maximale est celle demandée au LLM au choix du modèle ; si elle est inconnue (barre masquée), seul `/compact` fonctionne. Le contrôle a lieu avant l'envoi de chaque demande et après chaque tour.
L'appel est **bloquant** (pas de tâche de fond). Ctrl+C l'annule sans rien modifier ; en cas d'erreur, la conversation reste inchangée.
- Provider LiteLLM : validation sur un vrai serveur (streaming SSE et appels d'outils testés seulement sur des flux simulés) ; compatibilité de `@inkjs/ui` avec Ink 7 à confirmer à l'usage (l'affichage des badges a été vu fonctionner par l'utilisateur).
- Éventuellement rétablir sous `.maia` ce que supprimait l'abandon de `.claude` : variables d'environnement et slash-commandes personnalisées (le code des commandes existe encore dans `packages/agent/src/commands.ts`, il n'est plus appelé).
- Taille de contexte : lue en interrogeant le LLM (demande de l'utilisateur) ; la route `/model/info` de LiteLLM (`max_input_tokens`) serait plus fiable, et un réglage manuel dans `settings.json` pourrait corriger une réponse erronée.
- Nerd Fonts (icônes) : proposé comme réglage optionnel dans `settings.json`, non réalisé.
- Tests : les suites vitest et le typecheck sont à lancer après `pnpm install` (écrits sans avoir pu être exécutés dans l'environnement de développement).
## Travaux réalisés
Journal des évolutions, dans l'ordre :
1.**Squelette** : monorepo pnpm (TypeScript strict, tsup, vitest), contrats `LLMProvider` / `Tool` / `AgentUI`, boucle agent avec appels d'outils, agent par défaut de type « Claude Code » (outils Read, Write, Edit, Bash).
2.**Provider SAP AI Core** puis **choix du modèle** au démarrage et commande `/model`. *(Remplacé ensuite par LiteLLM, voir 12.)*
3.**Outils Glob et Grep** (JavaScript pur, sans ripgrep).
4.**Configuration de l'agent (lot 2)** : fichiers de contexte, permissions, slash-commandes, skills. *(Abandonné avec les dossiers `.claude`, voir 13.)*
5.**`§skill`** : appel direct d'un skill de l'agent.
6.**Dossier de lancement** : tous les chemins relatifs partent du dossier où MAIA est lancé (`MAIA_CWD` > `INIT_CWD` > `cwd`).
7.**Interface Ink** (remplace Clack.js) : bannière « MAIA » avec masque pixel-art et titre arc-en-ciel, messages en bleu clair, zone de saisie en pointillés `greenBright` avec spinner `arrow3`, attente « En réflexion » avec `dots10` en `yellowBright`.
8.**Outils TodoWrite et WebFetch** (lot 3, première partie).
9.**`!commande`** (avec la syntaxe `!'commande avec paramètres'`) et **`@fichier`**.
10.**Renommage en MAIA** (titre « Maia ») et **sessions** : `./.maia/.sessions`, sauvegarde automatique, `/rename`, `/load` (avec réaffichage du contenu), `/new`.
11.**Corrections** : import manquant de `normalizeSessionName`, `session.tools` est une `Map`, en-tête de skill avec espace avant les deux-points (`description : …`).
12.**LiteLLM** : un seul provider, requêtes au format OpenAI en streaming vers le endpoint OpenAI de LiteLLM ; configuration `~/.maia/settings.json` (`LiteLLM_URL`, `OpenAI_EndPoint`, `LiteLLM_Models`, `LiteLLM_API_Key` optionnel). Suppression de SAP AI Core, Anthropic, OpenAI et Ollama.
13.**Renommage technique**`magiccode` → `maia` (packages `@maia/*`, commande `maia`, variables `MAIA_*`, dossier `.maia`) ; textes « MagicCode » → « MAIA » ; bannière « Un seul agent, plusieurs modèles ».
14.**Skills dans `.maia/skills`** (dossier de lancement, dossier parent, dossier utilisateur) ; les dossiers `.claude` ne sont plus lus.
15.**Badges `@inkjs/ui`** : `INFO`, `WARN`, `FAIL` (texte d'erreur `#DC8ADD`) et `ASK` devant les questions.
16.**Suppression des modes et des extensions** : un seul agent MAIA (`packages/agent`), contexte de départ `MAIA.md` sinon `CLAUDE.md` (message `INFO`, ou `WARN` si aucun).
19.**Permissions granulaires** : règles `allow` / `ask` / `deny` par outil (`Outil` ou `Outil(motif)`), évaluées dans l'ordre deny > ask > allow, `ask` par défaut ; définies dans `~/.maia/settings.json` et `./.maia/settings.json` (pas le dossier parent).
20.**`/permissions`** : affiche les permissions chargées, par fichier et par type (badges ALLOW / ASK / DENY) ; les fichiers sont listés l'un sous l'autre au démarrage.
21.**Rendu markdown** des réponses de l'agent via `markdansi`, dans un seul composant isolé (`packages/ui/src/markdown.tsx`), désactivable avec `MAIA_NO_MARKDOWN=1`.
22.**Image de la bannière** : nouvelle image (visage à couronne), fond retiré, réduite à 32 × 32 en couleurs réelles (sans pixelisation ni réduction de palette), centrée verticalement à côté du titre ; données générées par `packages/ui/scripts/make-mask.py`.
23.**Barre de progression du contexte** sous la zone de saisie (1 carré = 10 000 tokens) : taille maximale demandée au LLM après la sélection du modèle, contexte utilisé mis à jour après chaque réponse (consommation renvoyée par LiteLLM, sinon estimation) ; correction d'un import de type manquant dans `ink-ui.tsx`. Valeur alignée sur le contexte réel de la session : contexte en attente compté, tokens de raisonnement exclus, estimation calibrée sur le serveur et signalée par `~`.
24.**Outils personnalisés** : `./.maia/tools/<nom>/TOOL.json` (dossier de lancement uniquement) ; paramètres `number`/`string` avec valeurs par défaut, `run` avec `{{paramètre}}` exécuté sans shell dans le dossier du tool (aucune injection possible), permission `ask` par défaut ; module `packages/agent/src/custom-tools.ts`, exemple `examples/tools/addition`.
25.**Threading async des outils** : 1 outil au premier plan ; dès 2 outils, une tâche de fond par outil, attente de toutes les fins avant de répondre au LLM (résultats dans l'ordre demandé, 8 en parallèle au plus, verrou par fichier).
26.**Compactage** : `/compact` et compactage automatique au seuil `Max_Token_Context - (4000 + 500)` ; résumé en 5 rubriques comme contexte de départ (à la place de MAIA.md / CLAUDE.md), session renommée `xxx__n` (cycle 1..1000), barre de contexte recalculée.