Document de référence des fonctionnalités **implémentées** à ce jour. Il décrit le comportement attendu, les contrats entre modules, les formats de fichiers et les constantes. Le `README.md` reste le guide d'utilisation ; ce document est destiné à qui développe ou maintient MAIA.
> État de vérification : les suites vitest, le typecheck, Ink et un vrai serveur LiteLLM n'ont jamais été exécutés dans l'environnement de développement (voir § 19). Les comportements décrits ici sont ceux du code ; ils sont couverts par des tests écrits mais à lancer après `pnpm install`.
---
## 1. Objet et périmètre
MAIA est un assistant IA en ligne de commande (CLI) :
- **un seul agent** (MAIA) ; il n'y a ni mode ni extension ;
- **accès aux LLM uniquement via LiteLLM**, au format OpenAI (`chat/completions`, streaming, appels d'outils) ;
- interface de terminal **Ink** (React) ;
- l'agent lit, cherche, modifie des fichiers et exécute des commandes dans le **dossier de lancement** grâce à des outils, soumis à des permissions ;
- sessions persistantes, skills, outils personnalisés, exécution parallèle des outils, compactage du contexte.
Hors périmètre actuel : sous-agents / outil Task, hooks, serveurs MCP, slash-commandes personnalisées (code présent mais non appelé), autocomplétion des chemins `@`, listes à choix multiples (voir § 18).
`maia [--model <spec>] [-h|--help]`. Un **TTY** est obligatoire (sinon message d'erreur, code de sortie 1).
### 5.2 Dossier de lancement
Base de **tous** les chemins relatifs (outils, `MAIA.md`, `.maia/…`, permissions). Résolu une fois au démarrage : `MAIA_CWD` > `INIT_CWD` (pnpm/npm) > `process.cwd()`. Il est affiché (« Dossier de travail : … »).
### 5.3 Résolution du modèle
Priorité : option `--model` > variable `MAIA_MODEL` > `./.maia/config.json` > `~/.maia/config.json` (propriété `model`). **Pas de défaut** : sans valeur, la liste des modèles LiteLLM est proposée. Forme acceptée : `litellm:<nom>` ou `<nom>` (le nom peut contenir `:` ou `/`).
### 5.4 Séquence de démarrage
1. Lecture de `~/.maia/settings.json` (voir § 7.3) ; erreur explicite avec exemple si absent/invalide, arrêt du programme.
2. Résolution de la configuration et du modèle (choix interactif si nécessaire).
3. Création de `./.maia` et `./.maia/.sessions` (échec → sessions désactivées, avertissement).
4.`initializeAgent` (contexte, skills, outils, permissions), création de la session d'agent.
5. Question « taille maximale du contexte » au LLM (§ 13.2).
6. Boucle de conversation (§ 5.5).
### 5.5 Boucle de conversation
À chaque itération : **comptage du contexte et mise à jour de la barre** → saisie (`readInput`) → traitement dans cet ordre :
1.`!commande` (§ 11.2) ;
2.`§skill` (§ 11.3) ;
3. commandes `/…` (§ 5.6) ;
4. sinon : prompt libre. Les références `@fichier` de la saisie sont résolues et ajoutées au contexte en attente (§ 11.4) ;
5. compactage automatique éventuel (§ 14.3) ;
6.`runTurn` avec un `AbortController` ; **Ctrl+C** interrompt la requête, restaure `messages` et `pendingContext` d'avant la demande et recompte le contexte ;
7. compactage automatique éventuel après le tour.
Sortie : `/exit`, Ctrl+D (saisie vide), Ctrl+C (saisie), SIGTERM, SIGHUP. La session est sauvegardée (écriture synchrone sur `exit`).
| `/model` | recharge la liste, change de modèle, conversation conservée ; redemande la taille de contexte |
| `/clear` | nouvel agent : MAIA.md / CLAUDE.md, skills, outils et permissions rechargés ; messages supprimés ; la session garde son nom |
| `/compact` | compactage (§ 14) |
| `/rename <nom>` | renomme la session courante |
| `/load [nom]` | recharge une session (liste de choix si sans argument) |
| `/new` | nouvelle session (nouveau contexte), l'ancienne est sauvegardée |
| `/exit` | quitte |
Toute autre commande : « Commande inconnue ». (Les slash-commandes personnalisées d'un agent sont prévues par le contrat `SlashCommand` mais l'agent MAIA n'en fournit plus.)
## 6. Boucle agent et exécution des outils (`core/src/agent.ts`, `tool-runner.ts`)
1. Le `pendingContext` est vidé et **joint** au texte utilisateur (`contexte\n\ntexte`) en **un seul message**`user` (certains providers exigent l'alternance user/assistant).
2. Boucle d'au plus **25 itérations** (`MAX_ITERATIONS`) :
- appel `provider.chat({ system, messages, tools })` (**bloquant** : le raisonnement du LLM n'est jamais mis en tâche de fond, pour préserver l'intégrité du contexte) ;
- le texte est diffusé via `ui.onText` ; le message `assistant` (texte + `tool_use`) est ajouté ; `ui.onContextUsage(sessionContextTokens(session))` ;
- sans appel d'outil → fin du tour ; sinon `runToolCalls`, ajout d'un message `user` portant les `tool_result`, itération suivante.
3. Au-delà de 25 itérations : message d'erreur « Arrêt : plus de 25 itérations ».
Les événements `usage` du provider sont ignorés par le cœur.
### 6.3 `runToolCalls` (exécuteur)
Les résultats sont **toujours renvoyés au LLM dans l'ordre des appels**.
1.*Phase 1 (séquentielle)* : pour chaque appel, autorisation (outil connu, règles de permissions, confirmation utilisateur **une par une**) ; chaque outil autorisé **démarre immédiatement** en arrière-plan.
2.*Phase 2* : `ui.onToolsRunning({ names, done })` à chaque fin d'outil, puis attente de **toutes** les tâches (`Promise.all`), puis `onToolsRunning(null)`.
3.*Phase 3* : affichage (`onToolCall` + `onToolResult`) et construction des `tool_result`, dans l'ordre des appels.
Règles :
- une exception, un refus ou un outil inconnu **n'annule pas** les autres outils ;
- au plus **8 outils simultanés** (`MAX_PARALLEL_TOOLS`, file FIFO `Limiter`) ;
-`Tool.lockKey` : deux appels de même clé s'exécutent **l'un après l'autre dans l'ordre demandé** (`KeyedQueue`). Clés actuelles : `file:<chemin absolu>` pour Read, Write, Edit ; `todowrite` pour TodoWrite. Une clé qui lève une exception est ignorée ;
- ce sont des **tâches asynchrones Node**, pas des threads du système (les processus enfants lancés par Bash ou les outils personnalisés sont, eux, réellement parallèles).
| Règle `deny` | `Action refusée par les règles de permissions (…). Ne réessaie pas telle quelle.` |
| `ask` (règle) ou, sans règle applicable, permission `ask` de l'outil, et refus utilisateur | `L'utilisateur a refusé cette action.` |
## 7. Accès aux LLM : LiteLLM (`packages/providers`)
### 7.1 Requête
`POST {LiteLLM_URL}{OpenAI_EndPoint}` (jointure sans doubler ni oublier le `/`), en-têtes `Content-Type: application/json`, `Accept: text/event-stream`, `Authorization: Bearer <clé>` si une clé est définie.
`parseSse` traite les lignes `data:` ; `[DONE]` et le JSON invalide sont ignorés ; un champ `error` lève `Erreur du serveur LiteLLM : …`. Le texte est émis au fil de l'eau ; les appels d'outils sont **accumulés par `index`** (nom et arguments concaténés) et émis **à la fin**, triés par index (arguments JSON invalides → objet vide, l'outil renverra une erreur explicite ; identifiant manquant → `call_N`). Le morceau `usage` (liste `choices` vide) devient l'événement `usage` (les tokens de raisonnement sont retirés de `outputTokens`).
`stopReason` : `tool_calls`/`function_call` → `tool_use` ; `length` → `max_tokens` ; `stop` → `tool_use` si des appels existent, sinon `end_turn` ; autre → `tool_use` ou `other`.
Assemblage tolérant : un fragment d'arguments **sans identifiant ni nom** sur un index inconnu est rattaché à l'appel précédent ; un **nouvel identifiant** sur un index déjà pris crée un nouvel appel (comportements observés chez certains fournisseurs derrière LiteLLM). Si le serveur termine par `length`, ou si un appel d'outil arrive avec une sortie de **exactement** 1024/2048/4096/8192/16384/32768/65536 tokens (serveur qui « répare » l'appel coupé et annonce `tool_calls`), le cœur avertit l'utilisateur (« Réponse coupée par la limite de tokens de sortie… »). Diagnostic : `MAIA_DEBUG_LLM=1` (ou un chemin de fichier) enregistre le flux SSE brut (défaut : `maia-llm-debug.log` dans le dossier temporaire).
Erreurs : serveur injoignable (`LiteLLM injoignable (url) : …`), HTTP non 2xx (500 premiers caractères du corps), réponse sans corps. Une annulation (`AbortSignal`) est relancée telle quelle.
### 7.3 `~/.maia/settings.json`
| Clé | Rôle |
| --- | --- |
| `LiteLLM_URL` (obligatoire) | URL du serveur |
| `OpenAI_EndPoint` (obligatoire) | suffixe de la requête de chat, ex. `/v1/chat/completions` |
| `LiteLLM_Models` (obligatoire) | suffixe de la liste des modèles, ex. `/v1/models` |
| `MaxOutputTokens` (facultatif) | entier positif : `max_tokens` envoyé à chaque requête (sauf si la requête fixe sa propre valeur, ex. le résumé de compactage à 4000) |
Ce fichier est dans le dossier de l'**utilisateur** (pas celui de lancement) ; il peut contenir un secret. La même propriété `permissions` y est lue (§ 9).
### 7.4 Liste des modèles
`GET {LiteLLM_URL}{LiteLLM_Models}` ; les noms sont lus dans `data[].id` (sinon `model_name`), dédoublonnés et triés ; valeur de choix `litellm:<nom>`. Les erreurs sont non bloquantes (avertissement) ; sans modèle disponible, message d'erreur.
Ordre de déclaration : Read, Glob, Grep, Write, Edit, Bash, TodoWrite, WebFetch, CurrentModel. Les chemins relatifs sont résolus depuis le dossier de lancement ; `~` et `~/…` désignent le dossier personnel ; un chemin complet est utilisé tel quel.
| **Bash** | `command` | exécute via `runShell` dans le dossier de lancement avec l'environnement de l'agent ; timeout **2 min** ; la commande tourne dans son propre groupe de processus : timeout et Ctrl+C arrêtent aussi ses sous-processus (SIGTERM puis SIGKILL après 1 s) ; stdout et stderr fusionnés dans l'ordre d'arrivée ; sortie coupée à **30 000** caractères ; dernière ligne `[code de sortie : N]`, `[délai dépassé (120 s)]`, `[interrompu (Ctrl+C)]` ou `[échec du lancement de la commande]` | ask | — |
| **TodoWrite** | liste de tâches (`pending` · `in_progress` · `completed`, forme `activeForm`) | remplace la liste à chaque appel ; rendu ☐ ▶ ☑ ; avertissement si plusieurs tâches « en cours » | auto | `todowrite` |
| **CurrentModel** | aucune | renvoie le modèle LLM actif (lu à l'appel) | auto | — |
**WebFetch.** URL http(s) uniquement. HTML → Markdown via `turndown` (titres `atx`, blocs de code `fenced`, puces `-`, listes à puce « - a » et numérotées « 1. a » par règle `listItem` personnalisée ; balises `head`, `script`, `style`, `noscript`, `iframe`, `svg`, `template` supprimées) ; JSON mis en forme ; texte brut. Limites : **5 Mo**, **30 s**, 5 redirections, `max_length` par défaut **20 000** caractères (plafond **100 000**). Une redirection vers un **autre domaine** n'est pas suivie : l'URL est renvoyée au modèle, qui doit la rappeler (nouvelle confirmation). L'en-tête de résultat contient l'URL, le type, le titre et la mention « Contenu externe non fiable… ». Aucun JavaScript exécuté, ni PDF, ni images.
Pour toute entrée invalide, l'outil lève une exception renvoyée comme erreur au modèle, qui liste les paramètres reçus (ex. `Paramètre "content" manquant ou invalide (…). Paramètres reçus : path (string).`). **Write** accepte aussi `contents`, `text`, `file_text`, `file_content`, `data` ou `body` à la place de `content`, et un tableau de chaînes (joint par des sauts de ligne).
`~/.maia/settings.json` (utilisateur) et `./.maia/settings.json` (dossier de lancement) — **pas** le dossier parent. Propriété `permissions` : `{ allow?: string[], ask?: string[], deny?: string[] }`. Les listes des deux fichiers s'additionnent. JSON invalide ou règle mal formée (`Outil` ou `Outil(motif)`, `Outil` = `[A-Za-z0-9_.-]+`) : avertissement, élément ignoré. Les dossiers `.claude` ne sont plus lus.
### 9.2 Évaluation
**deny, puis ask, puis allow** : la première famille qui correspond décide, quelle que soit la précision des règles. **Sans règle applicable : `ask`**, pour tous les outils (y compris ceux dont la permission par défaut est `auto`). Le verdict de la politique **remplace** la permission par défaut de l'outil quand il existe.
### 9.3 Motifs par outil
| Outil | Le motif porte sur |
| --- | --- |
| `Bash` | la commande : `*` = suite quelconque ; `cmd *` couvre `cmd` avec ou sans arguments (pas `cmdx`) ; sans `*` : commande exacte |
| `Read`, `Write`, `Edit`, `Glob`, `Grep` | le chemin (relatif au dossier de lancement, motifs glob) |
| `WebFetch` | `domain:hôte` exact ou avec `*` (le domaine nu ne couvre pas ses sous-domaines) |
| `Skill` | le nom du skill |
| autres outils (dont personnalisés) | une valeur texte de l'entrée (`*` accepté) |
### 9.4 Sécurité des commandes Bash composées
-`deny` et `ask` s'appliquent dès qu'**une** partie de la commande correspond (`&&`, `;`, `|`, substitutions `$(…)`, backticks, parenthèses, redirections — découpe fine) ;
-`allow` ne s'applique que si **toutes** les parties sont couvertes ;
- un `allow` ne couvre **jamais** une commande avec redirection (`>`) ou substitution : elle repasse en `ask`.
Les `deny` sont du « meilleur effort », pas un bac à sable. Les commandes `!` et les références `@` saisies par l'utilisateur **ne passent pas** par ces règles.
Au démarrage : message INFO (fichiers chargés, nombre de règles par fichier) ou « Aucune règle de permission définie : chaque utilisation d'un outil demande une confirmation ».
`./.maia/tools/<dossier>/TOOL.json` (`tool.json` accepté) — **dossier de lancement uniquement** (ni parent, ni utilisateur). Chargement alphabétique au démarrage, rechargé par `/clear` et `/new`. Le journal liste les outils valides sous « Outils personnalisés chargés : » (un par ligne).
| `name` | facultatif (nom du dossier par défaut) ; `^[A-Za-z0-9_-]{1,64}$` ; ne peut pas reprendre un nom intégré (Read, Bash… et `Skill`) ni un doublon |
| `parameters[].type` | `"number"` ou `"string"` |
| `parameters[].default` | valeur utilisée si le LLM n'en fournit pas ; sans `default`, le paramètre est **obligatoire** |
| `run` | ligne de commande ; `{{param}}` remplacé par la valeur ; `./` = dossier de l'outil, qui est aussi le dossier d'exécution |
Le `inputSchema` exposé au LLM est dérivé des paramètres (`number` → `number`, `string` → `string`). Une virgule en trop avant `]` ou `}` est tolérée. Une définition invalide (description absente, type inconnu, `{{x}}` non déclaré, JSON cassé…) donne un **WARN** et l'outil est ignoré, les autres restent chargés.
### 10.3 Exécution
- **Sans shell** (`execFile`) : la ligne `run` est découpée en arguments (guillemets simples ou doubles) ; chaque valeur envoyée par le LLM reste **un seul argument** (aucune injection : `; rm -rf /` ou `$(cmd)` sont reçus littéralement) ;
- le programme (premier mot) ne peut pas être un `{{paramètre}}` ; pas de tubes, redirections ni variables (appeler un script `sh ./script.sh {{p}}` pour cela) ; sous Windows, les `.cmd`/`.bat` ne sont pas lancés directement ;
- timeout **2 min**, sortie limitée à **30 000** caractères ; code de sortie non nul → erreur renvoyée au LLM avec la sortie d'erreur du script ;
- variables d'environnement ajoutées : `MAIA_CWD` (dossier de lancement) et `MAIA_TOOL_DIR` ;
- permission **`ask`** par défaut ; les règles de permissions s'appliquent avec le nom exact de l'outil (`"Addition"`, `"Addition(5)"`) ;
- les outils personnalisés font partie des outils du contexte (ils sont dans `tools` envoyé au LLM et comptés dans le contexte).
Exemple fourni : `examples/tools/addition/` (`TOOL.json` + `test.js`).
## 11. Contexte, skills, commandes utilisateur
### 11.1 Contexte de départ (`agent/src/memory.ts`, `init.ts`)
`MAIA.md` dans le dossier de lancement, sinon `CLAUDE.md` ; seul ce dossier est regardé. INFO si chargé, WARN si aucun. Le contenu est ajouté au **prompt système** sous `# Instructions (<chemin>)`.
Prompt système (en français) : identité MAIA, aide à la programmation en CLI, réponses concises dans la langue de l'utilisateur, lire avant de modifier, préférer Glob/Grep à Bash, TodoWrite pour ≥ 3 étapes, contenu WebFetch non fiable, rappel du dossier de travail ; puis le contexte (`MAIA.md` / `CLAUDE.md` ou résumé compacté), puis la section des skills.
Exécution (`runShell`) : shell du système, dossier de lancement, environnement de l'agent, timeout **120 s**, capture limitée à 1 000 000 de caractères, stdout et stderr dans l'ordre d'arrivée. La commande tourne dans **son propre groupe de processus** (hors Windows) : timeout ou Ctrl+C arrêtent toute l'arborescence (`SIGTERM` puis `SIGKILL` après 1 s ; `taskkill /T /F` sous Windows). `runShell` ne rejette jamais ; le statut est décrit par `describeShellStatus` (interrompu, échec de lancement, délai dépassé, signal, code de sortie).
Sans confirmation (c'est l'utilisateur qui la saisit). Le résultat est affiché (aperçu jusqu'à 4000 caractères) et ajouté au **contexte en attente** (jusqu'à 30 000 caractères, milieu tronqué), joint à la prochaine demande. Les programmes interactifs ne sont pas pris en charge.
### 11.3 Skills et `§`
Emplacements : `~/.maia/skills`, `<parent>/.maia/skills`, `<lancement>/.maia/skills` ; le plus spécifique l'emporte en cas de même nom. Un skill = dossier `<nom>/SKILL.md` (ou `skill.md`) avec un en-tête YAML : `description` (obligatoire), `name` (facultatif, doit égaler le nom du dossier, qui fait foi).
Le prompt système contient la liste (nom + description) ; le contenu est chargé à la demande par l'outil `Skill` (créé s'il y a des skills), ou lancé directement par `§nom [arguments]` (contrat `AgentSkill.invoke`, `$ARGUMENTS` / `ARGUMENTS:`). `§` seul liste les skills ; un skill inconnu donne une erreur avec la liste des disponibles.
Le contenu des fichiers cités est injecté dans le contexte de la demande (le texte de la saisie n'est pas modifié).
- Formes : `@chemin`, `@ chemin` (avec espace), `@'chemin avec espaces'` (`\'` = guillemet littéral), `@~/…`, `@/chemin/complet` ; un dossier injecte la liste de ses entrées (**200 maximum**).
- Un `@mot` qui ne désigne aucun fichier reste du texte (`@mention`, `jean@exemple.fr`) ; un avertissement n'apparaît que si le mot ressemble à un chemin.
- Limites : **2 Mo** par fichier, **50 000** caractères injectés par fichier (milieu tronqué), **150 000** par saisie ; binaires ignorés ; doublons injectés une fois.
- Le contenu est présenté au modèle comme une donnée, pas comme des instructions. Sans confirmation ni règle de permission.
- Les références ne sont résolues que pour la saisie de l'utilisateur (pas dans le corps d'un prompt de skill).
## 12. Sessions (`core/src/sessions.ts`)
### 12.1 Stockage
`./.maia/.sessions/<nom>.json`, créé avec `./.maia` au lancement. Écriture **atomique** (fichier temporaire `.tmp-<pid>` puis renommage). `.maia/` est ajouté au `.gitignore` du dépôt.
startContext?: string; // résumé compacté servant de contexte de départ (§ 14)
}
```
À la lecture, le **nom du fichier fait foi**, les champs manquants reçoivent des valeurs par défaut ; un fichier illisible ou au mauvais format lève une erreur claire (ignoré avec avertissement dans les listes).
### 12.2 Noms
Par défaut `session_AAAA-MM-JJ_HH_MM_SS` (heure locale). Un nom saisi est normalisé (espaces et guillemets externes retirés) et validé : non vide, ≤ 100 caractères, sans `\ / : * ? " < > |` ni caractère de contrôle, ne commence ni ne finit par `.`. Une session déjà existante sous un autre fichier fait ajouter un suffixe `_2`, `_3`… à l'enregistrement (jamais d'écrasement par inadvertance). Insensible à la casse pour la recherche (`find`) ; `suggest` propose des noms proches.
### 12.3 Cycle de vie
- sauvegarde à la sortie et avant tout changement de session ; **une session sans message ni `startContext` n'est jamais enregistrée** ; sauvegarde ignorée si rien n'a changé ;
-`/rename` : effectif à la sauvegarde, l'ancien fichier est supprimé (même session) ; changement de casse seul = renommage de fichier ;
-`/load` : sauvegarde la session quittée (message d'avertissement), recharge `messages` et `pendingContext`, **conserve le modèle courant**, reconstruit l'agent si `startContext` diffère, recompte le contexte et réaffiche l'historique (résultats d'outils tronqués à 600 caractères, messages utilisateur à 1500) ;
-`/new` : sauvegarde, nouvel agent (contexte rechargé), nouveau nom.
## 13. Taille du contexte et barre de progression
### 13.1 Comptage (`core/src/context.ts`)
`countContextTokens({ system, messages, tools?, pending? })` = somme de :
- le prompt système ;
- la définition des outils (`JSON.stringify(tools)`) ;
- pour chaque message : **4 tokens** de structure + le compte de chacun de ses blocs (texte ; `tool_use` = nom + arguments JSON ; `tool_result` = contenu) ;
- chaque texte du contexte en attente.
Tokenizer : `js-tiktoken/lite`, vocabulaire **`o200k_base`**, `encode(texte, [], [])` (un texte qui ressemble à un jeton spécial est compté comme du texte). Les comptes de blocs sont mémorisés (`WeakMap`) ; le prompt système et les outils ont une mémoire de la dernière valeur. Le calcul est **local, déterministe, et strictement additif** : ajouter un message ne peut qu'augmenter le total. La valeur ne diminue que si le contexte est réduit (`/clear`, `/compact`, tour interrompu, autre session). Pour les modèles qui n'utilisent pas ce vocabulaire, c'est une approximation.
`sessionContextTokens(session)` applique ce calcul à la session. Il est exécuté **avant chaque saisie**, après chaque réponse du LLM, au démarrage d'un agent et après chaque commande `!`. La consommation renvoyée par le serveur n'est plus utilisée (elle mélangeait deux comptes différents et faisait varier la valeur).
### 13.2 Taille maximale
Après le choix d'un modèle (démarrage, `/model`), MAIA pose au LLM la question « Taille maximale du contexte pour ce modèle, en tokens ? Réponds uniquement par un nombre entier… » (un appel par modèle et par exécution, timeout **60 s**, Ctrl+C l'abandonne). `parseTokenCount` extrait la valeur (`128000`, `128 000 tokens`, `131,072`, `1.000.000`, `200k`, `1M`, `1,5 million`, `2 millions`) ; plage plausible **1 000 – 100 000 000** ; une valeur avec unité l'emporte sur une valeur plus grande sans unité ; sinon la plus grande. La valeur est annoncée (INFO). Réponse illisible, erreur ou délai : avertissement, **barre masquée**, et le compactage automatique est désactivé (`/compact` reste disponible).
Sous la zone de saisie, en bas à droite : carrés ■ de gauche à droite, **1 carré = 10 000 tokens** (200 000 → 20 carrés), libellé `45.7k / 200k · 23%`. Au moins 1 carré rempli dès que le contexte n'est pas vide. Couleurs : vert, **jaune dès 60 %**, **rouge dès 85 %** ; carrés vides gris. Si la barre dépasse la largeur du terminal, l'échelle grandit par paliers de 10 000 et est indiquée (`1■ = 20k`). Format des nombres : `950`, `12.3k`, `200k`, `1.05M`.
Résumer toute la conversation, vider le contexte comme `/clear`, et utiliser le résumé **à la place** de `MAIA.md` / `CLAUDE.md` comme contexte de départ. L'appel est **bloquant** (pas de tâche de fond).
### 14.2 Déroulement (`compactNow`)
1. Rien à compacter (aucun message ni contexte en attente) → information, fin.
2. La session courante est sauvegardée telle quelle sous son nom.
3.`requestCompactSummary` : le LLM reçoit la conversation, plus un dernier message `user` = contexte en attente + prompt de résumé :
```
Faire un résumé compacté de nos échanges. Retourne le résultat sous le format suivant :
## Contexte du projet
- [Description brève du projet]
## Objectif actuel
- [Ce qu'on cherche à faire]
## Fichiers pertinents
- [Liste des fichiers importants]
## Décisions/Conclusions
- [Points clés établis]
## Prochaines étapes
- [Ce qui reste à faire]
```
Si le dernier message de la conversation est déjà `user`, le prompt lui est ajouté (alternance respectée). Le prompt système reçoit « n'appelle aucun outil » ; les outils restent déclarés (l'historique contient des appels d'outils) mais les `tool_use` reçus sont ignorés. `maxTokens` = **4000**. Un résumé vide → erreur ; arrêt par `max_tokens` → avertissement « résumé peut être incomplet ».
4. Nouvel agent (`startSession` avec `startContext = résumé`) : prompt système sans `MAIA.md`/`CLAUDE.md`, section « Contexte de départ : résumé compacté de la conversation précédente », outils/skills/permissions rechargés, `messages` vides, `pendingContext` vide.
5. Persistance (§ 14.4), puis affichage du résumé et du nouveau nom ; le contexte est recompté et la barre mise à jour.
En cas d'erreur ou de Ctrl+C **avant** le remplacement, la conversation est inchangée.
### 14.3 Déclenchement
- **Manuel** : `/compact`.
- **Automatique** : `Seuil_max = Max_Token_Context − (4000 + 500)` (4000 = tokens du résumé, 500 = marge d'erreur). Déclenché quand `contexte > Seuil_max` (strictement), si le maximum est connu et si le seuil est positif. Vérifié **avant l'envoi de chaque demande et après chaque tour** (pas au milieu d'un tour : un tour très long avec de nombreux outils peut dépasser le seuil avant d'être compacté). Un échec affiche un avertissement et la conversation continue.
### 14.4 Nom et persistance
`nextCompactedName(nom)` : `xxx` → `xxx__1` → `xxx__2` → … → `xxx__1000` → **`xxx__1`** (le fichier existant est écrasé). Un suffixe n'est un numéro de compactage que si `1 ≤ n ≤ 1000` (`xxx__0`, `xxx__1001`, `a__b` → suffixe ajouté). L'origine est tronquée si le nom dépasserait 100 caractères. La numérotation découle du nom de la session courante (renommer `xxx__2` en `foo` donne `foo__1`).
Le fichier d'avant compactage reste intact. Le fichier `xxx__n` contient `startContext` (le résumé) et la suite de la conversation ; il est écrit **immédiatement** (une session avec `startContext` n'est pas « vide »). `/load xxx__n` reconstruit l'agent avec ce contexte de départ et affiche le résumé.
## 15. Interface (`packages/ui`)
### 15.1 Architecture
`InkUI` (façade impérative : `info`, `warn`, `error`, `select`, `confirm`, `readInput`, `withSpinner`, `setBusy`, `showUser`, `onShell`, `showPermissions`, `setContextMax`, `setInterruptHandler`, et le contrat `AgentUI`) au-dessus d'un **`UIStore`** sans dépendance à React (testable) ; les composants React affichent l'état du store (`useSyncExternalStore`). Le journal garde les éléments par entrée ; aperçu des résultats d'outils **600** caractères, des sorties `!`**4000**.
### 15.2 Journal et badges (`@inkjs/ui`)
| Badge | Usage | Couleur du texte |
| --- | --- | --- |
| `INFO` | information | bleu clair `#7EC8FF` |
| `WARN` | avertissement | par défaut |
| `FAIL` | erreur | `#DC8ADD` |
| `ASK` | question (liste, confirmation) | gras |
Une ligne vide sépare chaque message. Une fois la réponse validée, la question est remplacée par `◆ Question : choix`.
### 15.3 Réponses de l'agent
Rendu markdown par `markdansi` (`render(markdown, { width })`) — **tout est dans `markdown.tsx`** (retrait : supprimer le fichier, la dépendance, et remplacer `<Markdown />` par `<Text>`). Le texte en cours de streaming reste brut ; la mise en forme apparaît à la fin de la réponse. `MAIA_NO_MARKDOWN=1` désactive le rendu ; un échec de `markdansi` retombe sur le texte brut.
### 15.4 Bannière
Image 32 × 32 en **couleurs réelles** (sans pixelisation ni réduction de palette), rendue en demi-blocs `▀▄█` (32 colonnes × 16 lignes), à gauche du titre « Maia » en grands caractères pixel (police 5 × 7) avec ombre et dégradé arc-en-ciel ; deux textes (vert : « Assistant IA en ligne de commande, propulsé par Ink et vos modèles de langage. » ; rose : « Un seul agent, plusieurs modèles »). Version complète à partir de **58 colonnes** ; entre 25 et 57 : sans le masque ; en dessous : cadre à double bordure compact, titre centré. Données du masque générées par `packages/ui/scripts/make-mask.py` (Pillow) depuis `assets/mask.png` vers `src/mask-data.ts`.
### 15.5 Saisie, attente, listes
- Zone de saisie : bordure en pointillés `┈`**au-dessus et au-dessous** seulement, `greenBright`, spinner `arrow3` à gauche, texte d'aide « Votre demande — /help : aide · § : skills · Ctrl+C : quitter » ; collage multi-lignes conservé ; historique ↑/↓.
- Attente après validation : « En réflexion » + spinner `dots10` en `yellowBright` (aussi pour chargement des modèles, exécution `!`, compactage, outils parallèles « Outils en parallèle : n/total terminés (…) »).
- Listes de choix : fenêtre de 10 lignes, ↑/↓ (ou j/k), Entrée valide, Échap annule.
- Confirmation d'action : `o` autorise, `n` ou Échap refuse, **Entrée seule est ignorée**.
- **Confirmation par défaut** de tout outil sans règle (`ask`) ; ordre deny > ask > allow ; analyse fine des commandes Bash composées (§ 9.4).
- **Outils personnalisés sans shell** : pas d'injection par les paramètres (§ 10.3).
- **WebFetch** : contenu signalé non fiable, pas de redirection inter-domaines suivie, limites de taille et de durée.
- Contenus injectés (`@fichier`, sorties `!`, WebFetch) présentés au modèle comme des **données** et non des instructions.
-`settings.json` utilisateur peut contenir une clé API : ne pas le versionner. Les `settings.json` d'un dépôt tiers peuvent contenir des règles `allow` larges : à relire.
- Les `deny` ne remplacent pas un bac à sable.
## 18. Limites connues et pistes
- **Provider LiteLLM non validé sur un vrai serveur** (SSE et appels d'outils testés sur des flux simulés) ; compatibilité `@inkjs/ui` / Ink 7 à confirmer à l'usage.
- La taille de contexte est obtenue en **interrogeant le LLM** (peut être fausse) ; la route `/model/info` de LiteLLM (`max_input_tokens`) ou un réglage manuel dans `settings.json` serait plus fiable.
- Comptage `o200k_base` : approximation pour les modèles non OpenAI.
- Compactage automatique contrôlé aux frontières de tour uniquement (§ 14.3).
- Parallélisme : tâches asynchrones, 8 simultanées, confirmations une par une.
- Sous-agents : l'effort borne les étapes (pas de `reasoning_effort` envoyé au modèle) ; pas d'imbrication ; une trace de sous-agent rechargée avec `/load` reprend la conversation dans l'agent principal (prompt système différent) ; chaque sous-agent recharge ses outils/skills/permissions depuis le disque (une modification en cours de session n'est pas vue par l'agent principal avant `/clear`).
- Non réalisés : hooks, serveurs MCP, variables d'environnement et slash-commandes personnalisées sous `.maia` (code de `commands.ts` présent mais non appelé), `@fichier` dans le corps d'un prompt de skill, autocomplétion des chemins `@`, listes à cocher, icônes Nerd Fonts.
Commandes : `pnpm install`, `pnpm test`, `pnpm typecheck`. Les suites ont été écrites et vérifiées par un lanceur de test minimal dans l'environnement de développement (sans vitest, Ink ni LiteLLM réels) ; leur exécution complète est à faire dans l'environnement du projet.
Dossiers : `~/.maia/agents` puis `./.maia/agents` (le projet remplace l'utilisateur à nom égal ; un seul passage si les deux sont le même dossier). Un dossier = un agent, fichier `AGENT.md`. Nom d'agent : lettres, chiffres, `_ . -`.
En-tête YAML (sous-ensemble : clés simples, listes `- a` ou `[a, b]`, table imbriquée `permissions`, commentaires `#`, clés insensibles à la casse) :
| Clé | Rôle | Défaut |
| --- | --- | --- |
| `name` | doit être le nom du dossier (sinon agent rejeté) | nom du dossier (avertissement) |
| `description` | obligatoire ; sert au LLM de l'agent principal à choisir | — |
| `model` | identifiant LiteLLM exact ou fragment | modèle de l'agent principal |
| `tools` | liste (virgules ou YAML) d'outils | tous, sauf `Agent` |
| `skills` | liste de skills | tous (`[]` : aucun) |
| `omitMd` | ne pas ajouter `MAIA.md` / `CLAUDE.md` | `false` |
| `trace` | enregistrer la conversation | `false` |
Corps : trois sections obligatoires, repérées par leurs titres Markdown (niveau libre, casse et accents ignorés ; « Instructions system(e) », « Missions », « Output ») ; une section s'arrête au titre suivant de même niveau ou plus haut, ou à une autre des trois sections ; les titres dans un bloc de code sont ignorés. Un agent invalide (nom, description, section manquante) est ignoré avec un avertissement précis. Journal au démarrage : « Agents chargés : » (un par ligne) ; commande `/agents`.
### 20.2 Lancement
Outil `Agent` (permission `ask`, règles `Agent` ou `Agent(nom)`), ajouté à l'agent principal quand au moins un agent existe ; sa description et le prompt système listent les agents. Paramètres : `agent` (énumération des noms), `task`. L'appel est bloquant et retourne « Résumé du sous-agent … » ; plusieurs appels `Agent` dans la même réponse passent par l'exécuteur parallèle (§ 6).
Le modèle est résolu par le parent (`resolveSubAgentModel` : identifiant exact, sinon nom contenant le fragment, sinon modèle du parent + avertissement ; liste LiteLLM chargée une fois, à la demande).
### 20.3 Dans le processus enfant
1.`initializeAgent({ subAgent })` recharge outils intégrés, outils personnalisés, skills et permissions depuis le disque, puis applique la définition : `tools` (insensible à la casse ; jamais `Agent`), `skills` (l'outil `Skill` n'existe que s'il reste des skills), permissions = règles héritées + règles de l'agent (union : un `deny`/`ask` hérité n'est jamais levé), contexte `MAIA.md`/`CLAUDE.md` sauf `omitMd`. Seuls les avertissements propres à la définition sont remontés (outil ou skill introuvable).
2. Prompt système : identité du sous-agent, autonomie (personne ne peut répondre), dossier de travail, indication d'effort, sections Instructions system / Missions / Output, consigne « la réponse finale respecte Output et suffit à elle-même », contexte du projet.
3.`runTurn(session, task, signal, { maxIterations })` : 10 / 25 / 50 itérations selon l'effort. `runTurn` retourne `"done"` ou `"max_iterations"`.
4. Résumé = texte de la dernière réponse sans appel d'outil ; sinon (budget épuisé, réponse vide) `requestFinalSummary` ajoute un message demandant le résumé selon `Output` (outils déclarés mais inutilisables, 4000 tokens) et l'inscrit dans la conversation.
5.`done` est envoyé, le processus se termine. Perte du canal IPC : le processus s'arrête.
Confirmations : l'interface de l'enfant n'affiche rien ; `confirm` est relayé au parent, qui pose la question (`[sous-agent nom] Autoriser … ?`) ; `InkUI.confirm` sérialise désormais toutes les questions (outils parallèles, plusieurs sous-agents).
### 20.4 Fin, erreurs, interruption
- Fin normale : INFO « Sous-agent « x » terminé en N s (M appels d'outil). » (+ nom du fichier de trace) ; le résumé est le `tool_result`.
- Échec (`ok: false`, processus arrêté sans résultat, lancement impossible) : avertissement, erreur d'outil renvoyée au LLM (avec la fin de la sortie d'erreur du processus).
- Interruption (Ctrl+C) ou délai de 30 min : message `abort` (l'enfant annule sa requête et refuse les confirmations en attente), `SIGKILL` après 5 s si le processus ne s'arrête pas.
### 20.5 Trace de session
Si `trace: true` (même en cas d'échec, s'il y a des messages) : `saveSubAgentTrace` écrit un `SavedSession` dans `./.maia/.sessions/` nommé `{session parente}-{agent}` (`subAgentSessionName` : `_2`, `_3`… si pris), avec les champs optionnels `agent` et `parentSession`. `/load` affiche « sous-agent x » dans la liste. Sans `trace`, rien n'est écrit.