651 lines
53 KiB
Markdown
651 lines
53 KiB
Markdown
# MAIA — Spécifications techniques
|
||
|
||
Document de référence des fonctionnalités **implémentées** à ce jour. Il décrit le comportement attendu, les contrats entre modules, les formats de fichiers et les constantes. Le `README.md` reste le guide d'utilisation ; ce document est destiné à qui développe ou maintient MAIA.
|
||
|
||
> État de vérification : les suites vitest, le typecheck, Ink et un vrai serveur LiteLLM n'ont jamais été exécutés dans l'environnement de développement (voir § 19). Les comportements décrits ici sont ceux du code ; ils sont couverts par des tests écrits mais à lancer après `pnpm install`.
|
||
|
||
---
|
||
|
||
## 1. Objet et périmètre
|
||
|
||
MAIA est un assistant IA en ligne de commande (CLI) :
|
||
|
||
- **un seul agent** (MAIA) ; il n'y a ni mode ni extension ;
|
||
- **accès aux LLM uniquement via LiteLLM**, au format OpenAI (`chat/completions`, streaming, appels d'outils) ;
|
||
- interface de terminal **Ink** (React) ;
|
||
- l'agent lit, cherche, modifie des fichiers et exécute des commandes dans le **dossier de lancement** grâce à des outils, soumis à des permissions ;
|
||
- sessions persistantes, skills, outils personnalisés, exécution parallèle des outils, compactage du contexte.
|
||
|
||
Hors périmètre actuel : sous-agents / outil Task, hooks, serveurs MCP, slash-commandes personnalisées (code présent mais non appelé), autocomplétion des chemins `@`, listes à choix multiples (voir § 18).
|
||
|
||
## 2. Pile technique
|
||
|
||
| Élément | Choix |
|
||
| --- | --- |
|
||
| Langage | TypeScript 5.6+, mode `strict` + `noUncheckedIndexedAccess`, ESM (`"type": "module"`) |
|
||
| Exécution | Node ≥ 20 (`engines`), `tsx` en développement |
|
||
| Gestionnaire de paquets | `pnpm@12.14.0` (champ `packageManager`), workspaces `packages/*` |
|
||
| Build | `tsup` (un seul bundle ESM, cible `node20`, shebang `#!/usr/bin/env node`) |
|
||
| Tests | `vitest` ^2.1 (`vitest run` à la racine) |
|
||
| Interface | `ink` ^7.0.3, `react` ^19.2, `@inkjs/ui` ^2 (badges), `cli-spinners` |
|
||
| Markdown | `markdansi` ^0.2 (isolé dans un composant) |
|
||
| Comptage de tokens | `js-tiktoken` ^1.0.21, vocabulaire `o200k_base` (`js-tiktoken/lite`) |
|
||
| HTML → Markdown | `turndown` ^7.2 |
|
||
| Fichiers | `ignore` (`.gitignore`), `picomatch` (motifs glob) |
|
||
|
||
`tsconfig.base.json` : `target ES2022`, `module ESNext`, `moduleResolution Bundler`, `jsx react-jsx`, `noEmit`.
|
||
|
||
## 3. Architecture
|
||
|
||
### 3.1 Paquets
|
||
|
||
| Paquet | Rôle | Dépend de |
|
||
| --- | --- | --- |
|
||
| `@maia/core` | Contrats (`LLMProvider`, `Tool`, `AgentUI`…), boucle agent, exécuteur d'outils, sessions, commande `!`, références `@`, invocation `§`, comptage de contexte, compactage, 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)
|
||
|
||
```ts
|
||
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`
|
||
|
||
```ts
|
||
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.
|
||
|
||
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 redirection (`>`) ou substitution : elle repasse en `ask`.
|
||
|
||
Les `deny` sont du « meilleur effort », pas un bac à sable. Les commandes `!` et les références `@` saisies par l'utilisateur **ne passent pas** par ces règles.
|
||
|
||
Au démarrage : message INFO (fichiers chargés, nombre de règles par fichier) ou « Aucune règle de permission définie : chaque utilisation d'un outil demande une confirmation ».
|
||
|
||
## 10. Outils personnalisés (`agent/src/custom-tools.ts`)
|
||
|
||
### 10.1 Chargement
|
||
|
||
`./.maia/tools/<dossier>/TOOL.json` (`tool.json` accepté) — **dossier de lancement uniquement** (ni parent, ni utilisateur). Chargement alphabétique au démarrage, rechargé par `/clear` et `/new`. Le journal liste les outils valides sous « Outils personnalisés chargés : » (un par ligne).
|
||
|
||
### 10.2 Définition
|
||
|
||
```json
|
||
{ "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.
|
||
|
||
```ts
|
||
interface SavedSession {
|
||
version: 1; name: string; createdAt: string; updatedAt: string; // ISO 8601
|
||
model: string; cwd: string;
|
||
messages: Message[]; pendingContext: string[];
|
||
startContext?: string; // résumé compacté servant de contexte de départ (§ 14)
|
||
}
|
||
```
|
||
|
||
À la lecture, le **nom du fichier fait foi**, les champs manquants reçoivent des valeurs par défaut ; un fichier illisible ou au mauvais format lève une erreur claire (ignoré avec avertissement dans les listes).
|
||
|
||
### 12.2 Noms
|
||
|
||
Par défaut `session_AAAA-MM-JJ_HH_MM_SS` (heure locale). Un nom saisi est normalisé (espaces et guillemets externes retirés) et validé : non vide, ≤ 100 caractères, sans `\ / : * ? " < > |` ni caractère de contrôle, ne commence ni ne finit par `.`. Une session déjà existante sous un autre fichier fait ajouter un suffixe `_2`, `_3`… à l'enregistrement (jamais d'écrasement par inadvertance). Insensible à la casse pour la recherche (`find`) ; `suggest` propose des noms proches.
|
||
|
||
### 12.3 Cycle de vie
|
||
|
||
- sauvegarde à la sortie et avant tout changement de session ; **une session sans message ni `startContext` n'est jamais enregistrée** ; sauvegarde ignorée si rien n'a changé ;
|
||
- `/rename` : effectif à la sauvegarde, l'ancien fichier est supprimé (même session) ; changement de casse seul = renommage de fichier ;
|
||
- `/load` : sauvegarde la session quittée (message d'avertissement), recharge `messages` et `pendingContext`, **conserve le modèle courant**, reconstruit l'agent si `startContext` diffère, recompte le contexte et réaffiche l'historique (résultats d'outils tronqués à 600 caractères, messages utilisateur à 1500) ;
|
||
- `/new` : sauvegarde, nouvel agent (contexte rechargé), nouveau nom.
|
||
|
||
## 13. Taille du contexte et barre de progression
|
||
|
||
### 13.1 Comptage (`core/src/context.ts`)
|
||
|
||
`countContextTokens({ system, messages, tools?, pending? })` = somme de :
|
||
|
||
- le prompt système ;
|
||
- la définition des outils (`JSON.stringify(tools)`) ;
|
||
- pour chaque message : **4 tokens** de structure + le compte de chacun de ses blocs (texte ; `tool_use` = nom + arguments JSON ; `tool_result` = contenu) ;
|
||
- chaque texte du contexte en attente.
|
||
|
||
Tokenizer : `js-tiktoken/lite`, vocabulaire **`o200k_base`**, `encode(texte, [], [])` (un texte qui ressemble à un jeton spécial est compté comme du texte). Les comptes de blocs sont mémorisés (`WeakMap`) ; le prompt système et les outils ont une mémoire de la dernière valeur. Le calcul est **local, déterministe, et strictement additif** : ajouter un message ne peut qu'augmenter le total. La valeur ne diminue que si le contexte est réduit (`/clear`, `/compact`, tour interrompu, autre session). Pour les modèles qui n'utilisent pas ce vocabulaire, c'est une approximation.
|
||
|
||
`sessionContextTokens(session)` applique ce calcul à la session. Il est exécuté **avant chaque saisie**, après chaque réponse du LLM, au démarrage d'un agent et après chaque commande `!`. La consommation renvoyée par le serveur n'est plus utilisée (elle mélangeait deux comptes différents et faisait varier la valeur).
|
||
|
||
### 13.2 Taille maximale
|
||
|
||
Après le choix d'un modèle (démarrage, `/model`), MAIA pose au LLM la question « Taille maximale du contexte pour ce modèle, en tokens ? Réponds uniquement par un nombre entier… » (un appel par modèle et par exécution, timeout **60 s**, Ctrl+C l'abandonne). `parseTokenCount` extrait la valeur (`128000`, `128 000 tokens`, `131,072`, `1.000.000`, `200k`, `1M`, `1,5 million`, `2 millions`) ; plage plausible **1 000 – 100 000 000** ; une valeur avec unité l'emporte sur une valeur plus grande sans unité ; sinon la plus grande. La valeur est annoncée (INFO). Réponse illisible, erreur ou délai : avertissement, **barre masquée**, et le compactage automatique est désactivé (`/compact` reste disponible).
|
||
|
||
### 13.3 Barre (`ui/src/context-model.ts`, `context-bar.tsx`)
|
||
|
||
Sous la zone de saisie, en bas à droite : carrés ■ de gauche à droite, **1 carré = 10 000 tokens** (200 000 → 20 carrés), libellé `45.7k / 200k · 23%`. Au moins 1 carré rempli dès que le contexte n'est pas vide. Couleurs : vert, **jaune dès 60 %**, **rouge dès 85 %** ; carrés vides gris. Si la barre dépasse la largeur du terminal, l'échelle grandit par paliers de 10 000 et est indiquée (`1■ = 20k`). Format des nombres : `950`, `12.3k`, `200k`, `1.05M`.
|
||
|
||
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 · ←/→, 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)` (réponse `o`/`y` ou `n`/Échap/Ctrl+C). 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` | outils personnalisés |
|
||
| `./.maia/agents/<nom>/AGENT.md` · `~/.maia/agents/<nom>/AGENT.md` | sous-agents |
|
||
| `<racine de MAIA>/.ressources/AGENT.md` | modèle d'agent de `/newAgent` (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 7 à confirmer à l'usage.
|
||
- La taille de contexte est obtenue en **interrogeant le LLM** (peut être fausse) ; la route `/model/info` de LiteLLM (`max_input_tokens`) ou un réglage manuel dans `settings.json` serait plus fiable.
|
||
- Comptage `o200k_base` : approximation pour les modèles non OpenAI.
|
||
- Compactage automatique contrôlé aux frontières de tour uniquement (§ 14.3).
|
||
- Parallélisme : tâches asynchrones, 8 simultanées, confirmations une par une.
|
||
- Sous-agents : l'effort borne les étapes (pas de `reasoning_effort` envoyé au modèle) ; pas d'imbrication ; une trace de sous-agent rechargée avec `/load` reprend la conversation dans l'agent principal (prompt système différent) ; chaque sous-agent recharge ses outils/skills/permissions depuis le disque (une modification en cours de session n'est pas vue par l'agent principal avant `/clear`).
|
||
- Non réalisés : hooks, serveurs MCP, variables d'environnement et slash-commandes personnalisées sous `.maia` (code de `commands.ts` présent mais non appelé), `@fichier` dans le corps d'un prompt de skill, autocomplétion des chemins `@`, listes à cocher, icônes Nerd Fonts.
|
||
|
||
## 19. Tests
|
||
|
||
Suites vitest (fichiers `*.test.ts`) : 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, repérées par leurs titres Markdown (niveau libre, casse et accents ignorés ; « Instructions system(e) », « Missions », « Output ») ; une section s'arrête au titre suivant de même niveau ou plus haut, ou à une autre des trois sections ; les titres dans un bloc de code sont ignorés. Un agent invalide (nom, description, section manquante) est ignoré avec un avertissement précis. Journal au démarrage : « Agents chargés : » (un par ligne) ; commande `/agents`.
|
||
|
||
### 20.1b Création : `/newAgent` et le modèle `.ressources/AGENT.md`
|
||
|
||
`/newAgent [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 / 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.
|
||
|
||
## 21. 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` |
|