> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fim.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Moteur DAG

> Fonctionnement du moteur DAG de FIM One — planification dynamique, exécution parallèle, analyse orientée vers les objectifs et re-planification autonome.

## Le pipeline

Le mode DAG décompose un objectif complexe en un graphe acyclique orienté d'étapes, les exécute avec un parallélisme maximal, puis réfléchit à savoir si l'objectif a été réellement atteint. Si ce n'est pas le cas, il replanifie et réessaie — de manière autonome, jusqu'à un budget configurable.

Le pipeline comporte quatre phases qui forment une boucle :

```mermaid theme={null}
flowchart TD
    A["1. History + Query -> enriched_query"] --> B["2. DAGPlanner"]
    B --> C["3. DAGExecutor (parallel steps)"]
    C --> D["4. PlanAnalyzer"]
    D --> E{Achieved?}
    E -->|"Yes"| F["5. stream_synthesize"]
    E -->|"No + confidence < threshold"| G["6. Replan"]
    G --> B
    E -->|"No + confident enough"| H["Fallback answer"]
```

**Planification.** Le LLM intelligent décompose la requête enrichie en 2-6 étapes avec des arêtes de dépendance explicites. Chaque étape reçoit une description de tâche, un conseil d'outil optionnel et un conseil de modèle qui contrôle si elle s'exécute sur le LLM rapide ou intelligent.

**Exécution.** Le DAGExecutor lance les étapes indépendantes en parallèle (jusqu'à 5 simultanément), en respectant le graphe de dépendance. Chaque étape s'exécute en tant qu'agent ReAct autonome sans mémoire — elle reçoit uniquement sa description de tâche et les résultats de ses dépendances complétées.

**Analyse.** Le PlanAnalyzer évalue si le plan exécuté a atteint l'objectif initial, produisant un verdict structuré : `achieved` (booléen), `confidence` (0.0-1.0), `reasoning`, et une `final_answer` optionnelle.

**Replanification.** Si l'objectif n'a pas été atteint et la confiance est inférieure au seuil d'arrêt, le pipeline boucle vers la planification avec un contexte de replanification qui résume ce qui s'est passé et ce qui n'a pas fonctionné. Cette boucle s'exécute jusqu'à `DAG_MAX_REPLAN_ROUNDS` fois de manière autonome.

Deux LLMs collaborent tout au long du processus : un **LLM intelligent** gère la planification, l'analyse et la synthèse des réponses (tâches nécessitant une capacité de raisonnement élevée), tandis que le **LLM général** gère l'exécution des étapes par défaut (les étapes avec `model_hint="fast"` étant déléguées au LLM rapide pour économiser les coûts). La compaction de contexte et le résumé de l'historique utilisent le LLM rapide. Chaque appel de sortie structuré utilise `structured_llm_call`, qui fournit une chaîne de dégradation à 3 niveaux (Native FC, JSON Mode, texte brut avec fallback regex) pour gérer les particularités de sortie spécifiques au modèle.

## Carte des appels LLM

Le pipeline DAG complet effectue sept catégories distinctes d'appels LLM. Comprendre où chaque appel se produit, quel modèle le gère et ce qui se passe en cas d'échec est essentiel pour le débogage et l'optimisation des coûts.

| # | Site d'appel             | Module            | Rôle LLM                                                                                                  | Format                | Secours                              |
| - | ------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------ |
| 1 | Résumé de l'historique   | chat.py           | LLM rapide                                                                                                | texte brut            | tronquer les 20K derniers caractères |
| 2 | DAGPlanner               | planner.py        | LLM intelligent                                                                                           | structured\_llm\_call | dégradation 3 niveaux                |
| 3 | Sélection d'outil        | react.py          | LLM étape                                                                                                 | structured\_llm\_call | retourner tous les outils            |
| 4 | Boucle ReAct (par étape) | react.py          | LLM général (par défaut) / LLM rapide (`model_hint="fast"`) / LLM raisonnement (`model_hint="reasoning"`) | chat()                | réessai/secours                      |
| 5 | ContextGuard compact     | context\_guard.py | LLM rapide                                                                                                | texte brut            | smart\_truncate                      |
| 6 | PlanAnalyzer             | analyzer.py       | LLM intelligent                                                                                           | structured\_llm\_call | regex + défaut                       |
| 7 | stream\_synthesize       | analyzer.py       | LLM intelligent                                                                                           | stream\_chat()        | analysis.final\_answer               |

```mermaid theme={null}
flowchart TD
    subgraph pre ["Pre-execution"]
        H["① History summary"] --> EQ["enriched_query"]
    end
    subgraph plan ["Planning"]
        P["② DAGPlanner<br/>structured_llm_call"]
    end
    subgraph exec ["Execution (per step)"]
        TS["③ Tool Selection<br/>structured_llm_call"] --> RL["④ ReAct Loop<br/>chat()"]
        CG["⑤ ContextGuard<br/>compact"]
    end
    subgraph post ["Post-execution"]
        PA["⑥ PlanAnalyzer<br/>structured_llm_call"] --> SS["⑦ stream_synthesize<br/>stream_chat()"]
    end
    EQ --> P
    P --> TS
    PA -->|"replan"| P
```

Les appels 1 et 5 sont **invisibles pour l'utilisateur** — ce sont des appels d'infrastructure qui gèrent la taille du contexte. Les appels 2, 6 et 7 utilisent le **LLM intelligent** car ils nécessitent une capacité de raisonnement élevée (décomposition des objectifs, jugement de réussite, synthèse de réponses cohérentes). L'appel 4 utilise le **LLM général** par défaut — seules les étapes explicitement marquées `model_hint="fast"` sont réduites au LLM rapide, et les étapes marquées `model_hint="reasoning"` sont promues au LLM raisonnement. L'appel 3 utilise le même LLM résolu pour cette étape.

## DAGPlanner

Le travail du planificateur est de transformer un objectif de haut niveau en un DAG valide d'étapes concrètes et exploitables. Il le fait avec un seul `structured_llm_call` vers l'LLM intelligent.

**Conception du prompt.** Le prompt de planification injecte la date et l'année actuelles (pour que l'LLM puisse planifier des recherches tenant compte du temps), applique la correspondance des langues (les descriptions de tâches doivent utiliser la même langue que l'objectif) et limite le nombre d'étapes à 2-6. Chaque étape a cinq champs : `id`, `task`, `dependencies`, `tool_hint` et `model_hint`. Le prompt décourage explicitement de diviser les sous-tâches trivialement liées — « si plusieurs vérifications peuvent être effectuées dans un seul script, combinez-les en UNE étape. »

**Extraction structurée.** Le planificateur utilise `structured_llm_call` avec un `_PLAN_SCHEMA` qui définit le schéma du tableau `steps` et une `parse_fn` qui convertit les dicts bruts en objets `PlanStep`. Si l'LLM retourne un objet d'étape unique au lieu d'un wrapper `{"steps": [...]}`, l'analyseur se rétablit automatiquement. La chaîne de dégradation à 3 niveaux documentée dans [ReAct Engine — structured\_llm\_call](/architecture/react-engine#structured_llm_call--unified-output-extraction) gère les particularités de sortie du modèle entre les fournisseurs.

**Validation du DAG.** Après extraction, le planificateur valide la structure du graphe en utilisant l'algorithme de Kahn pour le tri topologique. Deux invariants sont vérifiés :

1. **Pas de références en suspens.** Si une étape référence un ID de dépendance qui n'existe pas dans le plan, la référence est silencieusement supprimée avec un avertissement dans les logs. C'est un mécanisme de récupération — les LLMs omettent parfois les étapes qu'ils ont référencées, et un échec dur gaspillerait l'appel de planification entier.

2. **Pas de cycles.** Si l'algorithme de Kahn ne peut pas visiter tous les nœuds (ce qui signifie qu'au moins un cycle existe), le planificateur lève une `ValueError`. Les cycles sont irrécupérables — un plan cyclique ne peut pas être exécuté.

**model\_hint.** Le planificateur assigne `"fast"` aux étapes qu'il considère comme simples et déterministes (recherche de données, conversion de format, récupération directe), `null` aux étapes nécessitant un raisonnement standard (résolu vers le modèle général) et `"reasoning"` aux étapes nécessitant une analyse approfondie. L'exécuteur utilise cet indice pour sélectionner l'LLM approprié par étape via le `ModelRegistry`. En cas de doute, le prompt indique à l'LLM d'utiliser `null` — c'est toujours plus sûr d'utiliser le modèle plus capable. Pour les tâches spécifiques à un domaine (juridique, médical, financier), le planificateur reçoit le contexte du domaine du routeur et est guidé pour assigner `model_hint="reasoning"` aux étapes nécessitant une précision spécialisée.

**Construction de l'entrée.** La requête enrichie combine l'historique de conversation avec la demande actuelle. Si la conversation est longue, l'historique est chargé via `DbMemory` et formaté comme `"Previous conversation: ..."`. Lorsque la requête enrichie résultante dépasse 16K tokens (estimée via `CompactUtils.estimate_tokens`), elle est résumée par LLM en utilisant le prompt d'indice `planner_input` de ContextGuard avant d'être transmise au planificateur. Le secours quand aucun LLM rapide n'est disponible : tronquer brutalement aux 20K derniers caractères.

## DAGExecutor

L'exécuteur prend un `ExecutionPlan` validé et exécute ses étapes de manière concurrente, en respectant les dépendances et en appliquant les limites de ressources.

**Modèle de concurrence.** Un `asyncio.Semaphore` limite l'exécution parallèle des étapes à `max_concurrency` (5 par défaut, configurable via la variable d'environnement `MAX_CONCURRENCY`). La boucle de dispatch identifie toutes les étapes dont les dépendances sont terminées, les lance en tant qu'instances `asyncio.Task`, et attend qu'au moins une se termine avant de vérifier à nouveau. Les étapes sont lancées dans l'ordre des ID triés pour un comportement déterministe.

**Agent ReAct par étape.** Chaque étape s'exécute en tant qu'agent ReAct indépendant créé par `_resolve_agent()`. Si l'étape a un `model_hint` qui correspond à un rôle dans le `ModelRegistry`, un agent temporaire est créé avec le LLM correspondant. Sinon, le modèle par défaut (général) du registre est utilisé. Ces agents par étape n'ont **pas de mémoire** — ils commencent à zéro avec uniquement la description de leur tâche, l'objectif original, les indices d'outils, et les résultats des dépendances complétées. Cet isolement est intentionnel : les étapes du DAG doivent être des unités de travail autonomes qui ne fuient pas d'état à travers le graphe. Puisque chaque étape est une instance standard `ReActAgent`, elle hérite automatiquement de toutes les fonctionnalités du harnais : la [détection de cycle](/architecture/react-engine#cycle-detection) empêche une étape de boucler sur le même outil défaillant, et la [liste de vérification d'achèvement](/architecture/react-engine#completion-checklist) vérifie les conclusions avant qu'une étape ne finalise son résultat.

**Injection de contexte de dépendance.** `_build_step_context()` formate les résultats de toutes les étapes de dépendance complétées dans un bloc de texte : l'ID, le statut, la description de tâche et le résultat de chaque dépendance. Si un `ContextGuard` est configuré et que le contexte combiné dépasse `max_message_chars`, il est tronqué brutalement avec un suffixe `[Dependency context truncated]`. Cela empêche une étape qui dépend de plusieurs prédécesseurs verbeux de dépasser sa propre fenêtre de contexte.

**Multiplicateur de contenu structuré.** Lorsque les résultats de dépendance contiennent du contenu structuré — citations légales, tableaux markdown ou blocs de code — `_build_step_context()` applique un multiplicateur au budget de troncature (3.0 par défaut, configurable via `DAG_STRUCTURED_CONTEXT_MULTIPLIER`). Cela garantit que les citations, données tabulaires et autres artefacts structurés sont préservés à travers les limites des étapes plutôt que d'être tronqués au milieu d'une référence.

**Délai d'expiration de l'étape.** Chaque étape est enveloppée dans `asyncio.wait_for` avec un délai d'expiration par défaut de 600 secondes (10 minutes). Si une étape dépasse ce délai, elle est annulée et marquée comme `"failed"` avec un message de délai d'expiration. Le délai d'expiration est par étape, pas par plan — un plan à 5 étapes peut théoriquement s'exécuter pendant 50 minutes si les étapes s'exécutent séquentiellement.

**Interruption et annulation.** L'exécuteur a deux chemins d'annulation distincts, chacun déclenché par un événement différent :

```mermaid theme={null}
flowchart LR
    A["User follow-up message"] -->|"exec_stop_event.set()"| B["Pending steps → skipped\nRunning steps → complete normally"]
    C["HTTP client disconnects"] -->|"run_task.cancel()"| D["Running step tasks cancelled\nasyncio.gather awaits cleanup"]
```

*Saut gracieux — événement d'arrêt.* Lorsqu'un utilisateur envoie un message de suivi pendant l'exécution, l'orchestrateur dans `chat.py` définit `exec_stop_event`. L'exécuteur vérifie cet indicateur au début de chaque cycle de dispatch : s'il est défini, toutes les étapes `pending` restantes sont immédiatement marquées comme `"skipped"` avec la raison `"Skipped — user changed requirements"`, et la boucle se termine. Les étapes déjà en cours d'exécution sont autorisées à se terminer — seules les étapes non démarrées sont abandonnées. Cette sortie rapide permet au pipeline de se re-planifier autour de l'intention mise à jour de l'utilisateur sans attendre la fin du plan original complet.

*Abandon immédiat — annulation asyncio.* Lorsque le client HTTP se déconnecte, `chat.py` annule la `run_task` de niveau supérieur via `asyncio.Task.cancel()`. L'exécuteur capture `asyncio.CancelledError`, annule toutes les tâches d'étape actuellement en cours d'exécution, attend qu'elles reconnaissent via `asyncio.gather(..., return_exceptions=True)`, puis relève. La déconnexion du client est détectée en interrogeant `await request.is_disconnected()` toutes les 0,5 secondes à l'intérieur de la boucle d'événements SSE.

La différence sémantique est importante : **l'événement d'arrêt** signifie « ignorer ce qui n'a pas commencé, mais préserver ce qui s'exécute déjà » — les résultats des étapes complétées restent disponibles pour informer la re-planification. **CancelledError** signifie « abandonner tout immédiatement » — tout le travail en cours est supprimé sans récupération de résultat.

**Détection de blocage.** Si la boucle de dispatch ne trouve aucune tâche en cours d'exécution et aucune étape prête à être lancée (parce que leurs dépendances ont échoué), toutes les étapes pending restantes sont marquées comme `"failed"` avec un message expliquant que leurs dépendances ne se sont jamais complétées. Cela empêche l'exécuteur de se bloquer indéfiniment.

**Rappels de progression.** L'exécuteur déclenche des rappels `(step_id, event, data)` pour trois types d'événements : `"started"` (étape lancée), `"iteration"` (appel d'outil dans une étape), et `"completed"` (étape terminée). La couche SSE dans `chat.py` relie ces rappels aux événements `step_progress` que le frontend utilise pour afficher la visualisation du DAG en temps réel.

## Vérification des citations

Après chaque étape, l'exécuteur exécute optionnellement un **vérificateur de citations** qui contrôle les affirmations factuelles dans la sortie de l'étape. Ceci est contrôlé par la variable d'environnement `DAG_CITATION_VERIFICATION` (par défaut : `true`) et cible les domaines où les citations incorrectes comportent un risque élevé — les statuts juridiques, les références médicales et les réglementations financières.

Le vérificateur fonctionne en trois étapes :

1. **Extraction.** Les motifs regex identifient les chaînes ressemblant à des citations dans le résultat de l'étape (par exemple, numéros de dossier, références de statuts, codes de réglementation).
2. **Vérification.** Chaque citation extraite est évaluée par un appel de jugement LLM qui vérifie la plausibilité et la cohérence interne.
3. **Nouvelle tentative en cas d'échec.** Si la vérification échoue, l'étape est relancée avec un retour de correction ajouté à la description de la tâche, donnant à l'agent une chance de corriger les références inexactes.

La vérification des citations ajoute une latence aux étapes qui contiennent des citations, mais n'affecte pas les étapes sans citations. Désactivez-la en définissant `DAG_CITATION_VERIFICATION=false` si votre cas d'usage n'implique pas de domaines sensibles aux citations.

## Routage tenant compte du domaine

Le routeur automatique classe désormais les requêtes avec un **`domain_hint`** aux côtés de la sélection de mode existante. Les domaines reconnus sont `legal`, `medical` et `financial` ; les requêtes en dehors de ces domaines reçoivent `null`.

La classification de domaine influence le pipeline de deux façons :

**Mode DAG.** Lorsque le routeur sélectionne DAG et fournit un `domain_hint` non-null, le contexte de domaine est injecté dans l'invite du planificateur. Cela guide le planificateur vers l'attribution de `model_hint="reasoning"` pour les étapes qui nécessitent une précision spécialisée et suggère `tool_hint="read_skill"` pour les étapes qui correspondent aux compétences de domaine disponibles.

**Mode ReAct.** Lorsque le routeur sélectionne ReAct pour une requête spécifique à un domaine, le système passe du modèle général au **modèle de raisonnement** via `registry.get_by_role("reasoning")`. De plus, des instructions obligatoires sont injectées exigeant que l'agent utilise `web_search` avant de rédiger tout contenu spécifique au domaine et de vérifier les citations via la recherche. L'outil `read_skill` est épinglé dans la sélection d'outils (non filtré), garantissant que les connaissances de domaine sont toujours accessibles.

Le biais de routage se décale également : l'analyse de domaine étroitement couplée (où plusieurs sous-tâches partagent le contexte et les citations) préfère le mode ReAct pour éviter de perdre le contexte aux limites des étapes DAG.

## PlanAnalyzer

L'analyseur évalue si le plan exécuté a atteint l'objectif initial. Il produit un `AnalysisResult` structuré avec quatre champs :

* **`achieved`** (booléen) — `true` uniquement si l'objectif a été pleinement réalisé.
* **`confidence`** (float, 0.0-1.0) — le degré de certitude de l'analyseur dans son évaluation. Les sources qui se contredisent réduisent ce score.
* **`final_answer`** (chaîne ou null) — une réponse synthétisée lorsque l'objectif est atteint, `null` sinon.
* **`reasoning`** (chaîne) — la justification de la chaîne de pensée du LLM.

**Extraction structurée.** L'analyseur utilise `structured_llm_call` avec `_ANALYSIS_SCHEMA`, une `parse_fn` qui gère la coercition de type et le clamping de confiance, et une `regex_fallback` pour le JSON malformé. La regex fallback (`_regex_extract_analysis`) extrait les champs `achieved`, `confidence`, `final_answer` et `reasoning` du JSON partiellement valide en utilisant la correspondance de motifs. Cela importe car les réponses d'analyse ont tendance à être plus longues et complexes que les réponses de planification, rendant les erreurs de formatage JSON plus probables.

**Valeur par défaut sûre.** Si tous les niveaux d'extraction échouent (FC natif, mode JSON, texte brut, regex), l'analyseur retourne `AnalysisResult(achieved=False, confidence=0.0, reasoning="Could not parse analysis response")`. Cela garantit que le pipeline obtient toujours un résultat utilisable — une défaillance d'analyse devient un verdict « non atteint », qui déclenche une nouvelle planification plutôt que de causer un crash.

**Formatage du résultat d'étape.** Le résultat de chaque étape est tronqué à 10K caractères dans l'invite d'analyse. Cela empêche la sortie détaillée d'une seule étape (par exemple, un grand web scrape ou un dump de fichier) de dominer la fenêtre de contexte de l'analyseur et d'éclipser les résultats des autres étapes.

**Comparaison multi-sources.** L'invite d'analyse inclut une directive pour comparer explicitement les résultats de différentes sources. Lorsque les résultats de recherche web, la récupération de la base de connaissances et les opérations de fichiers contribuent tous des données, l'analyseur doit signaler les contradictions (nombres, dates, affirmations différents) et indiquer quelle source est probablement plus fiable. Les contradictions réduisent le score de confiance, ce qui à son tour influence la décision de nouvelle planification.

## Re-planification

La boucle de re-planification est la caractéristique la plus distinctive du moteur DAG : elle peut se rétablir de manière autonome des défaillances partielles en réfléchissant à ce qui s'est mal passé et en essayant une approche différente.

**Logique de décision.** Après chaque cycle plan-exécution-analyse, l'orchestrateur dans `chat.py` évalue le résultat de l'analyse :

1. **`achieved == True`** — quitter la boucle, procéder à la synthèse en streaming.
2. **Injection utilisateur survenue pendant ce cycle** — toujours re-planifier, indépendamment de la confiance ou du budget. Les messages de suivi de l'utilisateur sont traités comme des changements de spécifications qui exigent une nouvelle tentative. Cela ne consomme pas le budget de re-planification autonome.
3. **Budget de re-planification autonome épuisé** — quitter la boucle. Le budget est `max_replan_rounds - 1` re-planifications autonomes (par défaut : 2 re-planifications autonomes à partir d'un budget de 3 cycles au total).
4. **`confidence >= replan_stop_confidence`** — quitter la boucle. Même si l'objectif n'a pas été entièrement atteint, un score de confiance élevé (seuil par défaut : 0.8, configurable via `DAG_REPLAN_STOP_CONFIDENCE`) indique que l'analyseur est assez certain de ce qui s'est passé — la re-planification est peu susceptible d'aider.
5. **Sinon** — re-planifier. L'objectif n'a pas été atteint, la confiance est faible et le budget reste disponible.

**Contexte de re-planification.** Lors de la re-planification, l'orchestrateur appelle `_format_replan_context()` pour construire un résumé du cycle précédent. Cela inclut le raisonnement de l'analyseur et un aperçu tronqué du résultat de chaque étape (500 caractères maximum par étape). La troncature agressive est délibérée : le planificateur doit savoir *ce qui s'est passé* et *ce qui s'est mal passé*, pas les détails complets de la sortie de chaque étape. Ce contexte est transmis à `DAGPlanner.plan()` en tant que paramètre `context`, aux côtés de la requête enrichie originale.

**Cycles maximum.** La variable d'environnement `DAG_MAX_REPLAN_ROUNDS` (par défaut 3) contrôle le nombre total de cycles de planification. Avec les paramètres par défaut, le premier cycle est le plan initial, laissant jusqu'à 2 re-planifications autonomes. Les re-planifications déclenchées par l'utilisateur (via injection de message) ne comptent pas contre ce budget — un utilisateur peut diriger le pipeline indéfiniment.

**Événement SSE.** Lorsque le pipeline décide de re-planifier, il émet un événement de phase `replanning` contenant le raisonnement de l'analyseur. Le frontend l'utilise pour montrer à l'utilisateur pourquoi le pipeline réessaie.

**Accumulation d'enriched\_query.** Les messages de suivi de l'utilisateur sont ajoutés à la requête enrichie entre les cycles : `enriched_query += "\n\n[User follow-up]: {content}"`. Cela signifie que le planificateur voit l'évolution complète de l'intention de l'utilisateur — la demande originale plus toutes les clarifications ultérieures — lors de la construction d'un plan révisé.

## Synthèse en continu

Lorsque l'analyseur confirme que l'objectif a été atteint (`analysis.achieved == True`), le pipeline diffuse une réponse finale synthétisée à l'utilisateur via `PlanAnalyzer.stream_synthesize()`.

**Entrée.** L'appel de synthèse reçoit trois entrées : l'objectif original, les résultats des étapes formatés (10 000 caractères max par étape) et le raisonnement de l'analyseur provenant de l'appel d'analyse non-continu. Le raisonnement fournit une « feuille de route » pour ce que la synthèse doit couvrir.

**Invite système.** L'invite de synthèse demande au LLM de répondre directement sans méta-commentaires (« ne PAS inclure de phrases comme 'selon les résultats' »), de correspondre à la langue de l'objectif original et de comparer les résultats de différentes sources le cas échéant. Une directive de langue provenant des préférences utilisateur est ajoutée si disponible.

**Continu.** La méthode utilise `stream_chat()` pour produire les jetons de manière progressive. La couche SSE encapsule chaque bloc dans un événement `answer` avec `status: "delta"`, permettant au frontend un rendu en temps réel de la réponse finale.

**Chaîne de secours.** Deux chemins de secours gèrent les défaillances :

1. **stream\_synthesize lève une exception** — revenir à `analysis.final_answer` de l'appel `analyze()` non-continu. Cette réponse a déjà été générée lors de l'analyse, elle est donc disponible même si l'appel en continu échoue.

2. **Objectif non atteint (pas de synthèse tentée)** — concaténer tous les résultats des étapes complétées, séparés par des traits horizontaux. Chaque résultat est préfixé par son ID d'étape. Si aucune étape n'est complétée du tout, retourner `"(goal not achieved)"`.

La conception de secours garantit que l'utilisateur reçoit toujours une réponse — dégradée mais jamais vide.

## Architecture multi-LLM

Le profil de coût et de latence du moteur DAG est façonné par sa conception multi-modèle. La division du travail est :

| Rôle                    | Utilisé pour                                                            | Optimisé pour                    |
| ----------------------- | ----------------------------------------------------------------------- | -------------------------------- |
| **LLM intelligent**     | Planification, analyse, synthèse de réponses                            | Capacité de raisonnement         |
| **LLM général**         | Exécution d'étapes (par défaut), agent ReAct                            | Équilibre entre capacité et coût |
| **LLM rapide**          | Étapes `model_hint="fast"`, compaction de contexte, résumé d'historique | Coût et latence                  |
| **LLM de raisonnement** | Étapes `model_hint="reasoning"`, ReAct escaladé par domaine             | Capacité d'analyse approfondie   |

Le LLM intelligent gère les trois appels qui nécessitent le raisonnement le plus profond : décomposer un objectif en un plan cohérent, juger si le plan a atteint l'objectif, et synthétiser une réponse finale qui intègre de manière cohérente les résultats de plusieurs étapes. Ces appels se produisent une fois par tour (ou une seule fois pour la synthèse), de sorte que leur coût par jeton plus élevé est amorti.

Le LLM général gère l'exécution des étapes par défaut — la boucle ReAct de chaque étape DAG s'exécute sur le modèle général sauf si le planificateur attribue explicitement un `model_hint` différent. Le LLM rapide est réservé aux étapes marquées `model_hint="fast"` (recherches simples, conversions de format) et aux appels d'infrastructure (compaction de contexte, résumé d'historique). Le LLM de raisonnement est utilisé pour les étapes marquées `model_hint="reasoning"` et pour les tâches ReAct escaladées par domaine (voir [Routage sensible au domaine](#domain-aware-routing)).

**Remplacement par étape.** Le champ `model_hint` sur chaque `PlanStep` contrôle quel LLM exécute cette étape. Quand `model_hint` est `null`, l'exécuteur utilise le modèle général. Quand il est `"fast"`, l'exécuteur utilise le LLM rapide via le registre de modèles. Quand il est `"reasoning"`, l'exécuteur utilise le LLM de raisonnement. Le planificateur est instruit de définir `"fast"` pour les tâches déterministes, `null` pour le raisonnement standard, et `"reasoning"` pour l'analyse approfondie — mais il peut aussi être défini sur n'importe quel rôle personnalisé enregistré dans le `ModelRegistry`. La résolution du modèle se produit **une fois par étape** via `_resolve_agent()` immédiatement avant le début de la boucle ReAct de cette étape — toutes les itérations au sein de l'étape (sélection d'outil, boucle ReAct, compaction ContextGuard) utilisent le même LLM résolu. Le modèle ne change jamais en cours d'étape.

**Indépendance du budget.** Chaque rôle LLM dispose d'un budget de contexte indépendant, calculé à partir de sa configuration de modèle. L'exécution des étapes DAG utilise le budget du modèle d'étape résolu (général par défaut) ; les appels de planification et d'analyse utilisent le budget du LLM intelligent. C'est important car les opérateurs associent souvent un modèle à grand contexte (128K+) pour la planification avec différents modèles pour l'exécution des étapes. Pour plus de détails sur la façon dont les budgets sont calculés, voir [Gestion du contexte — Configuration du budget](/architecture/context-management#layer-5--budget-configuration).
