MagicCode/SPECS_TECH.md
2026-10-28 01:55:28 +01:00

64 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 : hooks, 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 ^9 (un seul bundle ESM, cible node20, shebang #!/usr/bin/env node)
Tests vitest ^2.1 (vitest run à la racine)
Interface ink ^8.0.0, react ^19.3.1, @inkjs/ui ^2 (badges), cli-spinners ^3.4
Markdown markdansi ^0.3.7 (isolé dans un composant)
Comptage de tokens js-tiktoken ^1.0.21, vocabulaire o200k_base (js-tiktoken/lite)
HTML → Markdown turndown ^7.2.4
Fichiers ignore ^7 (.gitignore), picomatch ^4 (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, résumé final de sous-agent — (+ 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, définitions et outil des sous-agents 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 50 itérations par série (MAX_ITERATIONS, réglable par MaxIterations) :
    • 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. À la limite : si l'appelant fournit onLimit (le CLI pose la question « Continuer ? (o/n) »), true ajoute une série d'itérations ; sinon (ou refus) message d'erreur « Arrêt : plus de N itérations » et retour "max_iterations". Une demande suivante est fusionnée dans le dernier message « utilisateur » (résultats d'outils) pour respecter l'alternance ; si Ctrl+C l'annule, le CLI restaure ce message.
  4. Stop : AgentUI.ask?(message): Promise<"yes"|"no"|"stop"> (repli sur confirm). Réponse « stop » à une confirmation d'outil → prepare renvoie stopped ; runToolCallsDetailed (runToolCalls en est la version qui ne renvoie que les résultats) fournit un résultat par appel (l'API exige un tool_result pour chaque tool_use) : STOP_FOR_MODEL pour l'appel visé, NOT_RUN_FOR_MODEL pour les suivants ; en parallèle, les outils déjà lancés finissent. runTurn pousse ces résultats puis retourne "stopped" sans rappeler le LLM et sans rien annuler ; la saisie suivante est fusionnée dans ce dernier message utilisateur (même mécanisme que la limite d'itérations). Le CLI affiche « Itération interrompue (stop) » ; Ctrl+C reste un rollback.

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
MaxIterations (facultatif) entier positif : itérations par série avant arrêt / question « Continuer ? » (défaut 50)
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 substitution ($(…), backticks, <(…), >(…)) : elle repasse en ask ;
  • Redirections de sortie : extractRedirections (guillemets et \\ respectés ; préfixes 2, & ; >>, >|, >&fichier ; 2>&1, >&2, >&- = duplications sans fichier) donne la commande sans redirections et les fichiers visés. Règles Redirect (sans motif : toutes ; Redirect(chemin) : motif de chemin relatif au dossier de lancement, comme Write). allow : la commande dépurée doit être couverte (règles Bash) ET chaque cible couverte par un Redirect allow ; une cible dynamique ($ * ? [ ] { }) n'est couverte que par Redirectsans motif ; analyse incertaine (guillemet non fermé, cible absente,>() → ask. deny/ask: une cible qui correspond à unRedirect(motif)suffit, quelle que soit la commande autorisée. Sans règleRedirect: toute redirection vers un fichier repasse enask`.

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é) et ~/.maia/tools/<dossier>/TOOL.json (utilisateur) — pas le dossier parent ; à nom égal, l'outil du projet l'emporte (celui de l'utilisateur est ignoré sans avertissement). 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 !. Si le flux contient usage.total_tokens (StreamEvent « usage », champ optionnel totalTokens), l'agent le mémorise après chaque réponse dans session.serverContext (total, comptage local au même instant, message ancre) : sessionContextTokens renvoie alors total + (local actuel − local mémorisé). La référence cesse d'être valable si le message ancre n'est plus à sa place (/clear, session rechargée, retour en arrière) : comptage local seul (localContextTokens). Sans total_tokens dans la réponse : comportement inchangé. Le total du serveur compte aussi les tokens de raisonnement.

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 (précision 100 tokens jusqu'au million, puis 1 000) : 950, 12.3k, 123.5k, 200k, 1.049M. Pendant le flux d'une réponse, runTurn envoie une estimation en direct (contexte avant l'appel + texte reçu + appels d'outils reçus), au plus toutes les LIVE_CONTEXT_INTERVAL_MS (250 ms) ; la valeur exacte suit à la fin de la réponse. Elle ne recule jamais.

Pendant un tour, runTurn appelle onContextUsage après l'ajout de la demande, après chaque réponse du LLM et après chaque série de résultats d'outils ; la barre est affichée en permanence en bas de l'écran (saisie, itérations, outils, confirmations, listes) ; taille maximale inconnue : « N tokens · taille maximale inconnue », sans carrés.

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

14.5 Contexte refusé par le serveur

isContextTooLongError(err) reconnaît les erreurs « input is too long », « prompt is too long », « context_length_exceeded », « maximum context length »… Dans la boucle de cli/src/index.ts, hors interruption et une seule fois par demande : avertissement, compactNow({ emergency: true }) (la demande de résumé utilise shrinkToolResults(messages, EMERGENCY_TOOL_RESULT_CHARS = 1500) pour tenir dans la fenêtre ; la session d'origine est sauvegardée avant), puis la demande est relancée avec une consigne de reprise sur la conversation compactée. Si le compactage échoue, l'erreur d'origine est affichée. Non couvert : sous-agents.

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 · Alt+Entrée (key.return + key.meta) ou Ctrl+J ; Maj+Entrée (key.shift) seulement si le terminal gère le protocole kitty (kittyKeyboard: { mode: "auto" }), sinon il est identique à Entrée ; (« \n » brut) ou la séquence ESC O M (livrée par Ink comme le bloc « OM ») : saut de ligne · ←/→ : déplacement · Home / Ctrl+A : début du prompt · End / Ctrl+E : fin du prompt (key.home / key.end d'Ink) · ↑/↓ : 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

La zone de saisie (cadre en pointillés) est toujours à l'écran : saisie active, grisée « Traitement en cours » pendant un tour (masquée à l'arrêt du programme), ou demande d'autorisation ASK … (o/n/s=stop) (réponse o/y, n/Échap/Ctrl+C, ou s = stop ; s n'est proposé que par ask, pas par confirm). Les listes de choix (/model, /load) remplacent la zone le temps du choix. Au-dessus, une ligne d'état est toujours réservée (spinner « En réflexion », outils en cours ; vide sinon) : la zone de saisie garde la même position. La barre de contexte est toujours en dessous.

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, ~/.maia/tools/<nom>/TOOL.json outils personnalisés (projet, utilisateur)
./.maia/agents/<nom>/AGENT.md · ~/.maia/agents/<nom>/AGENT.md sous-agents
<racine de MAIA>/.ressources/AGENT.md modèle d'agent de /createAgent (ressource privée de MAIA ; MAIA_RESSOURCES : autre dossier)
./.maia/.sessions/<nom>.json sessions (et traces de sous-agents <session>-<agent>.json)
./MAIA.md sinon ./CLAUDE.md contexte de départ
MAIA_MODEL, MAIA_CWD, INIT_CWD, LITELLM_API_KEY, MAIA_NO_MARKDOWN, MAIA_RESSOURCES 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 8 à 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, OAuth 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) : workflows autonomes (core/src/workflow.test.ts : registre, demandes, états ; cli/src/workflow-run.test.ts : modes wait / deny / allow, approve, RequestIntervention + reply, stop, sous-agent en échec, agents disponibles, commandes texte et JSON), serveurs MCP (mcp.test.ts : mcp.json, expansion ${VAR}, règles Mcp(...), faux serveur HTTP en JSON, SSE et ancien transport SSE, filtrage des outils, statuts OK/FAIL, aucune connexion si désactivé), sous-agents (agents.test.ts : en-tête, sections, chargement, modèle, outil, initialisation restreinte ; core/src/subagent.test.ts : résumé final, budget, trace ; cli/src/subagent-parent.test.ts : lanceur avec faux processus), 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. Sous-agents (agent/src/agents.ts, yaml-header.ts, subagent-tool.ts, cli/src/subagent-*.ts, core/src/subagent.ts)

20.1 Définition (AGENT.md)

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
permissions allow / ask / deny : listes de règles aucune règle ajoutée
effort low / medium / high medium
skills liste de skills tous ([] : aucun)
omitMd ne pas ajouter MAIA.md / CLAUDE.md false
trace enregistrer la conversation false

Corps : trois sections obligatoires et une facultative (Constraints / Contraintes, après Missions : AgentDefinition.constraints), repérées par leurs titres Markdown (niveau libre, casse et accents ignorés, précision finale entre parenthèses ignorée : « Constraints (optionnelle) » ; « Instructions system(e) », « Missions », « Constraints », « Output ») ; une section s'arrête au titre suivant de même niveau ou plus haut, ou à une autre section reconnue ; 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.1b Création : /createAgent et le modèle .ressources/AGENT.md

/createAgent [nom] (cli) : ui.select du niveau (« Projet » → ./.maia/agents, « Global » → ~/.maia/agents), nom en argument ou saisi (vide / Ctrl+C : annulation), validateAgentName (lettres, chiffres, _ . -, 64 caractères, pas de point initial), puis createAgentFromTemplate (agent) : lecture du modèle, applyAgentName (remplace uniquement la ligne name: de l'en-tête), création du dossier et écriture avec l'option wx (jamais d'écrasement : erreur « existe déjà »). Le modèle est cherché par findResource (cli/src/resources.ts) : $MAIA_RESSOURCES, sinon <racine de MAIA>/.ressources (3 niveaux au-dessus de packages/cli/dist ou src), sinon à côté du programme. Le modèle est un AGENT.md valide tel quel (name, description, effort, omitMd, trace actifs ; model, tools, permissions, skills en exemples #propriété: à décommenter) avec une explication par propriété en commentaires # et, dans le corps, des commentaires HTML par section.

Lecture de l'en-tête : les commentaires de fin de ligne ( # …, hors guillemets, # précédé d'un espace) sont retirés (stripComment) ; les commentaires HTML <!-- … --> des sections sont retirés avant l'envoi au modèle. Le nouvel agent est chargé au prochain démarrage, /clear ou /new.

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

Processus : child_process.fork(process.argv[1], ["--subagent"], { cwd, stdio: [ignore, pipe, pipe, ipc] }) (les execArgv sont hérités : tsx en développement). Protocole IPC (subagent-protocol.ts) :

Sens Message
parent → enfant start {spec} (définition, tâche, dossier, modèle résolu) · confirm-result {id, ok} · abort
enfant → parent confirm {id, message} · log {level, message} · done {ok, summary, messages, toolCalls}

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 / Constraints (si présente, suivie de la consigne : devant un cas prévu ou un interdit, ne pas agir, s'arrêter et le signaler dans le résumé) / 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.3b Lancement direct par l'utilisateur (&)

parseAgentInvocation(input) (core) : uniquement si la saisie commence par & ; &nom tâche ou & nom tâche, & seul = liste. Le CLI trouve l'agent (nom insensible à la casse), appelle directement le lanceur (pas d'outil Agent, donc pas de confirmation d'outil ; spinner « Sous-agent … au travail », Ctrl+C interrompt), affiche le résumé et l'ajoute à pendingContext (joint à la demande suivante, comme !). Sans tâche : DEFAULT_AGENT_TASK. Agent inconnu : erreur avec la liste.

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.

22. Serveurs MCP (agent/src/mcp*.ts)

Fichiers : mcp-config.ts (lecture de ~/.maia/mcp.json, expandEnv), mcp-rules.ts (règles Mcp(...), noms d'outils, sélection des serveurs : fonctions pures), mcp-client.ts (client JSON-RPC 2.0 minimal, transports Streamable HTTP et HTTP+SSE, analyseur SSE), mcp.ts (setupMcp, createMcpTool). Aucune dépendance ajoutée (client écrit à la main sur fetch).

Déroulement (initializeAgent, après le chargement des permissions) :

  1. loadMcpConfig(homeDir, env) : ~/.maia/mcp.json uniquement (ni projet, ni parent). Serveur invalide (clé hors [A-Za-z0-9_-] ou contenant __, url absente ou non http(s), type ≠ http/sse) : avertissement, ignoré. enabled:false : ignoré. ${VAR} développé dans url et headers.
  2. selectMcpServers(rules, clés) (le serveur * désigne tous les serveurs : Mcp(*), Mcp(*,outil)) : un serveur est activé s'il a une règle allow Mcp(clé…) et aucune règle deny Mcp(clé) / Mcp(clé,*). Règle sur une clé inconnue : avertissement. Aucune connexion avant ce tri.
  3. Connexion en parallèle des serveurs activés (initialize, notifications/initialized, tools/list paginé). Échec (réseau, HTTP, délai, JSON-RPC) : statut fail + avertissement, les autres serveurs et MAIA continuent.
  4. Outils : mcp__<clé>__<outil> (nom nettoyé en [A-Za-z0-9_-], ≤ 64 caractères, doublons ignorés avec avertissement), permission: "ask" ; retirés : excludeTools, hors includeTools, règle deny Mcp(clé,outil).

Permissions (permissions.ts) : une règle Mcp(clé[,motif]) s'applique à tout appel d'outil mcp__clé__outil (allow = sans confirmation, ask, deny). L'ancienne forme mcp__clé__outil(motif de valeur) reste valide.

Client : Mcp-Session-Id mémorisé et renvoyé, MCP-Protocol-Version: 2025-03-26, Accept: application/json, text/event-stream ; réponse JSON ou flux SSE lue jusqu'à l'id attendu. Transport sse : GET url, évènement endpoint, POST vers cet endpoint, réponses lues sur le flux. Délai timeout (s) par requête ; pour sse, jusqu'à la réception de endpoint. Résultat d'outil : parties text concaténées (autres types signalés), isError → exception, tronqué à 100 000 caractères.

Interface : /mcp → UIStore.showMcp (LogKind "mcp") : badge OK vert / FAIL rouge + nom ; cause de l'échec en grisé. AgentInitResult.mcpServers (états) et close() (fermeture des connexions : session suivante, /exit, Ctrl+D, SIGTERM/SIGHUP ; Streamable HTTP : DELETE de la session, SSE : fermeture du flux ; au plus 3 s en cas de signal).

Limites : pas d'OAuth ; outils seulement ; les sous-agents (processus enfants) n'ont pas les outils MCP ; connexions refaites à chaque redémarrage de session (/clear, /new, /load, /model, /compact) ; pas de reconnexion automatique en cours de session ; notifications serveur (tools/list_changed) ignorées.

23. Workflows autonomes (core/src/workflow.ts, agent/src/orchestrator.ts, cli/src/workflow-run.ts, cli/src/workflow-cli.ts)

Entrée : cli/src/index.ts aiguille maia run|workflows|workflow|logs|result|approve|reply|stop vers runWorkflowCli(args) avant toute interface (pas d'Ink, pas de TTY requis) ; run se termine par process.exit(code).

Agent parent (loadOrchestrator) : AGENT.md lu par parseAgentFile (nom = dossier), plus timeout (parseDuration : ms, s, m, min, h, d ; nombre seul = minutes) et permissionMode (wait|deny|allow). orchestratorLayout(file) : dossier des agents = parent du dossier de l'agent ; s'il se termine par .maia/agents, dossier de travail = le projet au-dessus, sinon le dossier de lancement. loadOrchestratedAgents : loadAgentsFrom([~/.maia/agents, dossier du parent]) (le second l'emporte), sans le parent. mentionedAgents : agents dont le nom apparaît dans les sections du parent → état initial pending. initializeOrchestrator : contexte MAIA.md / CLAUDE.md (sauf omitMd), skills (filtre skills), outils intégrés + personnalisés + MCP (filtre tools, sans jamais retirer Agent ni les outils du workflow), Agent, RequestIntervention ; permissions = settings.json + règles de l'agent ; RequestIntervention toujours autorisé. Prompt : buildOrchestratorPrompt (rôle d'orchestrateur, parallèle / séquence, intervention par l'outil et jamais par une question en texte, sections, résultat final = Output). Itérations : EFFORT_ITERATIONS[effort].

Exécution (runWorkflow(opts, deps), sans exception : l'échec est dans le résultat et le registre) : réglages → parent → modèle (--model > model > DefaultModel > premier modèle LiteLLM ; fragment résolu par resolveSubAgentModel) → workflow créé (ou repris : --workflow-id en mode détaché) → lanceur de sous-agents habituel (createSubAgentLauncher, délai porté au maximum : c'est le timeout du workflow qui borne) avec onAgentState (états) et onTranscript (traces) → runTurn puis finalAssistantText / requestFinalSummary → result.md. Échec avant le démarrage (fichier, modèle) : workflow enregistré error. Interface sans affichage : onToolCall / erreurs d'outils → journal ; confirm → askPermission(agent, message) : allow / deny immédiats ; wait → createRequest (permission), agent waiting, interrogation du fichier de réponse toutes les pollMs (1 s). Les confirmations des sous-agents arrivent par l'IPC existant, avec le nom de l'agent (LauncherDeps.confirm(message, agent)). Elles restent posées une à la fois. Arrêt : maia stop dépose stop ; le workflow le voit (même intervalle) et annule le signal du tour (sous-agents interrompus par le lanceur) ; runTurn vérifie le signal avant chaque appel au modèle. Délai timeout : même mécanisme. Code 130 (arrêt) / 1 (délai, erreur). AgentBook : un AgentRecord par agent (runs, active pour les lancements parallèles du même agent, waits) ; running/waiting selon les attentes, done/error quand le dernier lancement finit ; à la fin du workflow, tout agent encore actif passe error.

Registre (WorkflowRegistry(homeDir), ~/.maia/workflows/<id>/, id wf_AAAAMMJJ_HHMMSS_xxxx) : workflow.json, agents.json (écriture atomique : fichier temporaire + renommage), events.jsonl (ajouts), requests/rN.json + rN.answer.json (réponse unique, écrite par approve / reply), stop, traces/<agent>[-n].json, result.md, run.log. Un seul écrivain par fichier. find(ref) : id exact, last, morceau unique, nom (le plus récent). deriveWorkflowState : done/error définitifs ; processus absent (process.kill(pid, 0), EPERM = vivant) → error ; un agent waiting → waiting ; sinon running.

--detach : le workflow est créé par la commande (id affiché tout de suite), puis spawn(process.execPath, [...execArgv, argv[1], "run", …, "--workflow-id", id, "--quiet"], { detached, stdio → run.log }) + unref().

Commandes : texte (couleurs si TTY et sans NO_COLOR) ou --json (state = code stable pending|running|waiting|done|error, label = libellé français). workflow --watch : rafraîchi toutes les 2 s jusqu'à la fin ; logs -f : toutes les secondes. Codes : 0 ; 2 = mauvaise utilisation ou référence introuvable.

Limites : pas d'API HTTP (fichiers + ligne de commande) ; un seul niveau de sous-agents ; confirmations sérialisées (une demande en attente à la fois par processus) ; pas de nettoyage automatique des anciens workflows ; un workflow détaché s'arrête si la machine redémarre (état en erreur, processus disparu).

24. Annexe — constantes

Constante Valeur Lieu
Itérations par série d'un tour (réglable : MaxIterations) 50 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
Itérations d'un sous-agent (low / medium / high) 10 / 25 / 50 agents.ts
Délai maximal d'un sous-agent / délai avant SIGKILL 30 min / 5 s subagent-parent.ts
Tokens du résumé final d'un sous-agent 4000 core/src/subagent.ts
Aperçu d'un résultat d'outil / d'une sortie ! 600 / 4000 caractères store.ts
Workflows : intervalle de lecture des réponses / de l'arrêt · rafraîchissement --watch / logs -f 1 s · 2 s / 1 s workflow-run.ts, workflow-cli.ts
MCP : délai par défaut / résultat d'outil / longueur d'un nom d'outil 30 s / 100 000 caractères / 64 mcp-config.ts, mcp-client.ts, mcp-rules.ts