58 KiB
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 (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, 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
- Lecture de
~/.maia/settings.json(voir § 7.3) ; erreur explicite avec exemple si absent/invalide, arrêt du programme. - Résolution de la configuration et du modèle (choix interactif si nécessaire).
- Création de
./.maiaet./.maia/.sessions(échec → sessions désactivées, avertissement). initializeAgent(contexte, skills, outils, permissions), création de la session d'agent.- Question « taille maximale du contexte » au LLM (§ 13.2).
- 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 :
!commande(§ 11.2) ;§skill(§ 11.3) ;- commandes
/…(§ 5.6) ; - sinon : prompt libre. Les références
@fichierde la saisie sont résolues et ajoutées au contexte en attente (§ 11.4) ; - compactage automatique éventuel (§ 14.3) ;
runTurnavec unAbortController; Ctrl+C interrompt la requête, restauremessagesetpendingContextd'avant la demande et recompte le contexte ;- 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?)
- Le
pendingContextest vidé et joint au texte utilisateur (contexte\n\ntexte) en un seul messageuser(certains providers exigent l'alternance user/assistant). - Boucle d'au plus 50 itérations par série (
MAX_ITERATIONS, réglable parMaxIterations) :- 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 messageassistant(texte +tool_use) est ajouté ;ui.onContextUsage(sessionContextTokens(session)); - sans appel d'outil → fin du tour ; sinon
runToolCalls, ajout d'un messageuserportant lestool_result, itération suivante.
- appel
- À la limite : si l'appelant fournit
onLimit(le CLI pose la question « Continuer ? (o/n) »),trueajoute 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. - Stop :
AgentUI.ask?(message): Promise<"yes"|"no"|"stop">(repli surconfirm). Réponse « stop » à une confirmation d'outil →preparerenvoiestopped;runToolCallsDetailed(runToolCallsen est la version qui ne renvoie que les résultats) fournit un résultat par appel (l'API exige untool_resultpour chaquetool_use) :STOP_FOR_MODELpour l'appel visé,NOT_RUN_FOR_MODELpour les suivants ; en parallèle, les outils déjà lancés finissent.runTurnpousse 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.
- 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.
- Phase 2 :
ui.onToolsRunning({ names, done })à chaque fin d'outil, puis attente de toutes les tâches (Promise.all), puisonToolsRunning(null). - Phase 3 : affichage (
onToolCall+onToolResult) et construction destool_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 FIFOLimiter) ; 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 ;todowritepour 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
denyetasks'appliquent dès qu'une partie de la commande correspond (&&,;,|, substitutions$(…), backticks, parenthèses, redirections — découpe fine) ;allowne s'applique que si toutes les parties sont couvertes ;- un
allowne couvre jamais une commande avec substitution ($(…), backticks,<(…),>(…)) : elle repasse enask; - Redirections de sortie :
extractRedirections(guillemets et\\respectés ; préfixes2,&;>>,>|,>&fichier;2>&1,>&2,>&-= duplications sans fichier) donne la commande sans redirections et les fichiers visés. RèglesRedirect(sans motif : toutes ;Redirect(chemin): motif de chemin relatif au dossier de lancement, commeWrite).allow: la commande dépurée doit être couverte (règlesBash) ET chaque cible couverte par unRedirectallow ; une cible dynamique ($* ? [ ] { }) n'est couverte que parRedirectsans 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 lignerunest 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 scriptsh ./script.sh {{p}}pour cela) ; sous Windows, les.cmd/.batne 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) etMAIA_TOOL_DIR; - permission
askpar 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
toolsenvoyé 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
@motqui 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
startContextn'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), rechargemessagesetpendingContext, conserve le modèle courant, reconstruit l'agent sistartContextdiffè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.
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)
- Rien à compacter (aucun message ni contexte en attente) → information, fin.
- La session courante est sauvegardée telle quelle sous son nom.
requestCompactSummary: le LLM reçoit la conversation, plus un dernier messageuser= 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é quandcontexte > 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, spinnerarrow3à 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
dots10enyellowBright(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 :
oautorise,nou É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 |
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.jsonutilisateur peut contenir une clé API : ne pas le versionner. Lessettings.jsond'un dépôt tiers peuvent contenir des règlesallowlarges : à relire.- Les
denyne 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/infode LiteLLM (max_input_tokens) ou un réglage manuel danssettings.jsonserait 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_effortenvoyé au modèle) ; pas d'imbrication ; une trace de sous-agent rechargée avec/loadreprend 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 decommands.tsprésent mais non appelé),@fichierdans le corps d'un prompt de skill, autocomplétion des chemins@, listes à cocher, icônes Nerd Fonts.
19. Tests
Suites vitest (fichiers *.test.ts) : 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
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 ; jamaisAgent),skills(l'outilSkilln'existe que s'il reste des skills), permissions = règles héritées + règles de l'agent (union : undeny/askhérité n'est jamais levé), contexteMAIA.md/CLAUDE.mdsaufomitMd. Seuls les avertissements propres à la définition sont remontés (outil ou skill introuvable).- 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.
runTurn(session, task, signal, { maxIterations }): 10 / 25 / 50 itérations selon l'effort.runTurnretourne"done"ou"max_iterations".- Résumé = texte de la dernière réponse sans appel d'outil ; sinon (budget épuisé, réponse vide)
requestFinalSummaryajoute un message demandant le résumé selonOutput(outils déclarés mais inutilisables, 4000 tokens) et l'inscrit dans la conversation. doneest 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),SIGKILLaprè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) :
loadMcpConfig(homeDir, env):~/.maia/mcp.jsonuniquement (ni projet, ni parent). Serveur invalide (clé hors[A-Za-z0-9_-]ou contenant__,urlabsente ou non http(s),type≠ http/sse) : avertissement, ignoré.enabled:false: ignoré.${VAR}développé dansurletheaders.selectMcpServers(rules, clés)(le serveur*désigne tous les serveurs :Mcp(*),Mcp(*,outil)) : un serveur est activé s'il a une règle allowMcp(clé…)et aucune règle denyMcp(clé)/Mcp(clé,*). Règle sur une clé inconnue : avertissement. Aucune connexion avant ce tri.- Connexion en parallèle des serveurs activés (
initialize,notifications/initialized,tools/listpaginé). Échec (réseau, HTTP, délai, JSON-RPC) : statutfail+ avertissement, les autres serveurs et MAIA continuent. - Outils :
mcp__<clé>__<outil>(nom nettoyé en[A-Za-z0-9_-], ≤ 64 caractères, doublons ignorés avec avertissement),permission: "ask"; retirés :excludeTools, horsincludeTools, règle denyMcp(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. 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 |
| 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 |