MagicCode/SPECS_TECH.md
Arnaud 9f3badd79d Update .gitignore and add SPECS_TECH.md with project modifications
- Add HOW_To_Agents.md, generated_page.md, llm_avis.md to .gitignore
- Add SPECS_TECH.md technical specifications
- Add test files for bash and write tools
- Update various core, agent, provider and UI components
2026-10-20 01:45:23 +02:00

42 KiB
Raw Blame History

MAIA — Spécifications techniques

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).

2. Pile technique

Élément Choix
Langage TypeScript 5.6+, mode strict + noUncheckedIndexedAccess, ESM ("type": "module")
Exécution Node ≥ 20 (engines), tsx en développement
Gestionnaire de paquets pnpm@12.14.0 (champ packageManager), workspaces packages/*
Build tsup (un seul bundle ESM, cible node20, shebang #!/usr/bin/env node)
Tests vitest ^2.1 (vitest run à la racine)
Interface ink ^7.0.3, react ^19.2, @inkjs/ui ^2 (badges), cli-spinners
Markdown markdansi ^0.2 (isolé dans un composant)
Comptage de tokens js-tiktoken ^1.0.21, vocabulaire o200k_base (js-tiktoken/lite)
HTML → Markdown turndown ^7.2
Fichiers ignore (.gitignore), picomatch (motifs glob)

tsconfig.base.json : target ES2022, module ESNext, moduleResolution Bundler, jsx react-jsx, noEmit.

3. Architecture

3.1 Paquets

Paquet Rôle Dépend de
@maia/core Contrats (LLMProvider, Tool, AgentUI…), boucle agent, exécuteur d'outils, sessions, commande !, références @, invocation §, comptage de contexte, compactage — (+ js-tiktoken)
@maia/providers Adaptateur LiteLLM (format OpenAI, SSE), liste des modèles, settings.json utilisateur core
@maia/ui Interface Ink : store sans React, composants, bannière, barre de contexte, rendu markdown core
@maia/agent L'agent MAIA : initialisation, MAIA.md/CLAUDE.md, skills, outils intégrés, outils personnalisés, permissions core
@maia/cli Point d'entrée : arguments, configuration, boucle de conversation, commandes /… tous

Règle de dépendance : core n'importe jamais ui, providers ni agent. L'interface est injectée sous forme du contrat AgentUI.

3.2 Flux général

saisie ─► cli (commandes / ! @ §) ─► runTurn(core)
                                        │  ┌───────────────────────────┐
                                        ├─►│ provider.chat (LiteLLM)   │  bloquant
                                        │  └───────────────────────────┘
                                        └─► runToolCalls (1 = avant-plan, ≥ 2 = parallèle)
                                              └─► Tool.execute (agent)
                      AgentUI (ui) ◄── événements : texte, appels, résultats, contexte

4. Contrats (packages/core/src/types.ts)

4.1 Messages (format neutre, indépendant des providers)

type ContentBlock =
  | { type: "text"; text: string }
  | { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
  | { type: "tool_result"; toolUseId: string; content: string; isError?: boolean };
interface Message { role: "user" | "assistant"; content: ContentBlock[] }

4.2 LLMProvider

{ id, model, capabilities: { tools }, chat(req, signal?) → AsyncIterable<StreamEvent> }.

ChatRequest = { system, messages, tools: ToolSpec[], maxTokens? }. StreamEvent :

Événement Sens
text_delta { text } morceau de texte de la réponse
tool_use { id, name, input } appel d'outil complet (émis à la fin du flux)
usage { inputTokens, outputTokens } consommation renvoyée par le serveur (ignorée pour la barre de contexte, voir § 13)
done { stopReason } end_turn · tool_use · max_tokens · other

4.3 Tool

interface Tool {
  spec: { name; description; inputSchema /* JSON Schema */ };
  permission: "auto" | "ask";           // permission par défaut de l'outil
  describe?(input): string;             // résumé court (UI, confirmation)
  lockKey?(input, { cwd }): string | undefined; // exclusion mutuelle (voir § 6.3)
  execute(input, ctx: ToolContext): Promise<string>; // ctx = { cwd, model?, signal?, env? }
}

Un outil renvoie une chaîne ; une exception est convertie en résultat d'erreur (isError: true) par l'exécuteur.

4.4 AgentUI

onThinking(bool), onText, onTextEnd, onToolCall(name, summary), onToolResult(name, output, isError), confirm(message) → Promise<boolean>, et les optionnels onContextUsage?(tokens) et onToolsRunning?({ names, done } | null).

4.5 Initialisation de l'agent

InitContext = { cwd, homeDir, log, warn, startContext? }. ModeInitResult = { systemPrompt, tools, commands?, skills?, permissions?, permissionSources?, env? }.

AgentSkill = { name, description, invoke(args) → string | Promise<string> }.

5. Démarrage et configuration (packages/cli)

5.1 Ligne de commande

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).

5.6 Commandes slash

Commande Effet
/help aide (commandes, skills)
/permissions règles chargées, par fichier (badges ALLOW / ASK / DENY)
/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)

6.1 AgentSession

{ provider, system, tools: Map<string, Tool>, commands, skills, permissions?, permissionSources, env?, messages, startContext?, pendingContext: string[], ui, cwd }.

6.2 runTurn(session, userText, signal?)

  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 appel (ou 0) — avant-plan. Séquence : annonce (onToolCall) → permission → confirmation éventuelle → exécution → onToolResult.

2 appels ou plus — parallèle.

  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).

Autorisation d'un appel (prepare) :

Cas Résultat renvoyé au LLM
Outil inconnu Outil inconnu : <nom> (erreur)
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.

Corps (buildChatBody) : { model, messages, stream: true, stream_options: { include_usage: true }, [max_tokens], [tools] }.

Correspondance du format neutre vers OpenAI :

Format MAIA Format OpenAI
system message role: "system" en tête
assistant texte + tool_use role: "assistant", content (ou null), tool_calls[{ id, type: "function", function: { name, arguments: JSON } }]
user avec tool_result un message role: "tool" (tool_call_id) par résultat, placés avant le texte utilisateur éventuel (role: "user")
ToolSpec tools[{ type: "function", function: { name, description, parameters } }]

7.2 Réponse (SSE)

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
LiteLLM_API_Key (facultatif) clé API ; sinon variable LITELLM_API_KEY
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.

8. Outils intégrés (packages/agent/src/tools)

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.

Outil Entrée Comportement Permission par défaut lockKey
Read path, offset? (1), limit? (2000) contenu avec numéros de ligne (6 colonnes + tabulation) auto fichier
Write path, content crée ou écrase le fichier (crée les dossiers parents) ask fichier
Edit path, old_string, new_string remplacement unique : erreur si old_string est absent ou présent plusieurs fois ask fichier
Glob pattern, path? fichiers correspondants, 100 résultats maximum, JavaScript pur auto —
Grep pattern (regex JS), path?, glob?, output_mode? (files_with_matches · content · count), -i, -n, -A, -B, -C, head_limit? (250 ; 0 = illimité) ignore .git, node_modules, fichiers du .gitignore, binaires ; fichiers > 2 Mo ignorés ; lignes coupées à 500 caractères auto —
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
WebFetch url, max_length? voir ci-dessous ask —
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).

9. Permissions (agent/src/permissions.ts, settings.ts)

9.1 Sources

~/.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 ».

10. Outils personnalisés (agent/src/custom-tools.ts)

10.1 Chargement

./.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).

10.2 Définition

{ "name": "Addition", "description": "…",
  "command": { "parameters": [ { "name": "param1", "type": "number", "default": "0", "description": "…" } ],
               "run": "node ./test.js {{param1}}" } }
Champ Règle
description obligatoire
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[].name identifiant ^[A-Za-z_][A-Za-z0-9_]*$
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.

11.2 Commande ! (core/src/shell.ts, cli/src/user-shell.ts)

Saisie Commande
!ls -la tout ce qui suit !
!'ls -la' texte entre guillemets simples
!'ls' -la src texte entre guillemets + ce qui suit : ls -la src
!'echo "l\'ami"' \' = guillemet simple littéral
!'ls -la erreur (guillemet non fermé), rien d'exécuté
! aide

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.

11.4 Références @fichier (core/src/fileref.ts)

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.

interface SavedSession {
  version: 1; name: string; createdAt: string; updatedAt: string; // ISO 8601
  model: string; cwd: string;
  messages: Message[]; pendingContext: string[];
  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).

13.3 Barre (ui/src/context-model.ts, context-bar.tsx)

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.

14. Compactage (core/src/compact.ts + cli/src/index.ts)

14.1 Principe

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.
Contexte Touches
Saisie Entrée : envoyer · ←/→, Ctrl+A/E : déplacement · ↑/↓ : historique · Ctrl+U/W : effacer · Ctrl+C ou Ctrl+D (saisie vide) : quitter
Pendant un tour Ctrl+C : interrompt et restaure la conversation d'avant la demande

16. Fichiers et variables

Chemin / variable Rôle
~/.maia/settings.json LiteLLM (§ 7.3) + permissions utilisateur
~/.maia/config.json model par défaut de l'utilisateur
~/.maia/skills/ skills de l'utilisateur
./.maia/config.json model du projet
./.maia/settings.json permissions du projet
./.maia/skills/ · <parent>/.maia/skills/ skills
./.maia/tools/<nom>/TOOL.json outils personnalisés
./.maia/.sessions/<nom>.json sessions
./MAIA.md sinon ./CLAUDE.md contexte de départ
MAIA_MODEL, MAIA_CWD, INIT_CWD, LITELLM_API_KEY, MAIA_NO_MARKDOWN variables d'environnement

17. Sécurité

  • 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.
  • Non réalisés : sous-agents + outil Task, 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.

19. Tests

Suites vitest (fichiers *.test.ts) : boucle agent et permissions (agent.test.ts), exécuteur d'outils parallèle (tool-runner.test.ts : durée, ordre, confirmations séquentielles, refus, lockKey, plafond de 8), comptage et taille de contexte (context.test.ts), compactage et noms __n (compact.test.ts), sessions, ! (shell.test.ts), @ (fileref.test.ts), skills, outils (chemins, Glob/Grep, TodoWrite, WebFetch, CurrentModel), outils personnalisés (31 tests dont exécution réelle de scripts), initialisation de l'agent, provider LiteLLM (corps de requête, SSE), interface (store, barre de contexte, pixels, couleurs, spinner, markdown), dossier de lancement.

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.

20. Annexe — constantes

Constante Valeur Lieu
Itérations maximales d'un tour 25 agent.ts
Outils simultanés 8 tool-runner.ts
Timeout ! / Bash / outil personnalisé 120 s shell.ts, bash.ts, custom-tools.ts
Sortie Bash / outil personnalisé 30 000 caractères bash.ts, custom-tools.ts
Contexte ! joint à la demande 30 000 caractères shell.ts
Capture ! 1 000 000 caractères shell.ts
@fichier : fichier / par fichier / par saisie / entrées de dossier 2 Mo / 50 000 / 150 000 / 200 fileref.ts
Read : lignes par défaut 2000 read.ts
Glob : résultats 100 glob.ts
Grep : fichier max, ligne max, head_limit 2 Mo, 500 caractères, 250 grep.ts
WebFetch : octets / durée / redirections / caractères (défaut, plafond) 5 Mo / 30 s / 5 / 20 000, 100 000 webfetch.ts
Tokens de structure par message 4 context.ts
Échelle de la barre 10 000 tokens par carré ; jaune ≥ 60 %, rouge ≥ 85 % context-model.ts
Plage valide d'une taille de contexte 1 000 – 100 000 000 context.ts
Question de taille de contexte : délai 60 s cli/src/index.ts
Tokens du résumé / marge / seuil 4000 / 500 / max − 4500 compact.ts
Compactages numérotés 1 – 1000 (cyclique) sessions.ts
Nom de session : longueur maximale 100 sessions.ts
Largeur minimale de la bannière complète 58 colonnes banner.tsx
Aperçu d'un résultat d'outil / d'une sortie ! 600 / 4000 caractères store.ts