7i. Delegate Sub-Agent — Sous-agent Stateful

delegate_tool.py (v1.3) implémente un système de sous-agents ECHO stateful avec accès aux outils. Contrairement à l'ancienne delegate_reasoning() (supprimée en cognitive_agents.py v5.14), le sous-agent dispose d'une boucle agentique complète identique au Pipe principal, d'un budget d'appels de fonctions configurable et d'un mécanisme de reprise via identifiant de session (sub_sid). L'état et la cascade de cette boucle peuvent être visualisés en temps réel via l'action HUD Sub-Agent Monitor.

💡 Delegate vs. delegate_reasoning (supprimé)

L'ancienne delegate_reasoning() était un appel LLM stateless (une seule requête sans outil). Le nouveau delegate_to_subagent est stateful : il maintient un historique de conversation persisté en SQLite (cognitive_threads), peut appeler des outils réels, changer de niveau cognitif (LITE → FLASH → PRO) et poser des questions à l'orchestrateur via le protocole QUESTION:. La migration est opaque pour Gemini grâce à la mise à jour de la docstring de l'outil.

Les 4 fonctions LLM exposées

Fonction Rôle Paramètres clés
delegate_to_subagent Délégation principale : crée ou reprend un thread de sous-agent task, system_prompt, sub_sid (opt.), with_context_distillate (opt.)
list_subagent_sessions Liste les threads delegate actifs pour le chat courant
close_subagent_session Ferme et purge définitivement un thread (irréversible) sub_sid
summarize_subagent_session Résumé structuré d'un thread via distillation LLM (≤ 8192 tokens) sub_sid

⚠️ Fonctions hors portée du sous-agent (DELEGATE_SUBAGENT_BLACKLIST)

Certains outils sont exclus de la liste transmise au sous-agent, pour prévenir les récursions infinies, la corruption de la mémoire long terme et les erreurs d'état. La blacklist est définie dans echo_constants.py (§1.5) :

Persistance — Threads SQLite

L'état du sous-agent (historique des échanges) est persisté dans la table cognitive_threads de la base {chat_id}.db via EchoStateManager. Chaque thread est identifié par un sub_sid au format dlg_{10 chars hexadécimaux} (ex: dlg_a3f9b2c1d0).

Un thread peut être repris à tout moment en passant sub_sid à delegate_to_subagent. L'orchestrateur peut ainsi répondre à une QUESTION: du sous-agent, ou reprendre une tâche longue interrompue.

Résolution des callables — Architecture tri-sources

OWUI présente un problème architectural fondamental : les callables Python des outils ne sont pas transmis nativement aux tool callables ni au Pipe via les paramètres standards. ECHO v5.166 résout ce problème via une architecture à trois sources de résolution, par ordre de priorité :

Résolution des callables — delegate_tool.py v1.3 [Tri-sources]
flowchart TD START([delegate_to_subagent appelé]) --> S1 S1["Source 1 : _TOOLS_CACHE[chat_id]\n(module-level dans pipe_engine.py)\nCallables OWUI originaux — optimal"] S1 --> CHK1{Cache peuplé ?} CHK1 -->|Oui| FILTER["Filtrage DELEGATE_SUBAGENT_BLACKLIST\n→ sub_tools final"] CHK1 -->|Non| S2["Source 2 : __metadata__['_echo_tools_dict']\n(normalement vide — OWUI ne partage pas)"] S2 --> CHK2{Dict non vide ?} CHK2 -->|Oui| FILTER CHK2 -->|Non| S3["Source 3 : _resolve_sub_tools_from_sys_modules()\nScan sys.modules → instanciation Tools\n→ injection user_valves → méthodes async"] S3 --> FILTER FILTER --> LOOP["Boucle agentique\n_run_subagent_loop()"] classDef optimal fill:#064e3b,stroke:#10b981,color:#fff classDef fallback fill:#78350f,stroke:#f59e0b,color:#fff classDef last fill:#7f1d1d,stroke:#ef4444,color:#fff class S1 optimal class S2 fallback class S3 last

Source 1 — _TOOLS_CACHE (Bridge Pipe)

Le Pipe (pipe_engine.py) stocke __tools__ dans un dictionnaire module-level _TOOLS_CACHE[chat_id] à chaque invocation. Ce cache est accessible depuis delegate_tool via sys.modules["function_pipe_engine"]. C'est la source principale en production : elle contient les callables OWUI originaux (des functools.partial pré-configurés avec les paramètres d'infrastructure).

Source 3 — _resolve_sub_tools_from_sys_modules()

Dernier recours si les deux premières sources échouent. La fonction scanne sys.modules à la recherche des modules tool_*, instancie chaque classe Tools, injecte les user_valves depuis __user__["valves"] et récupère toutes les méthodes async publiques comme callables.

Limitation Source 3

Les instances créées par la Source 3 sont fraîches (valves par défaut). Seules les user_valves sont injectées depuis __user__["valves"]. Les valves de type Valves (admin) restent aux valeurs par défaut. Cette différence est sans impact pratique dans la quasi-totalité des cas.

Boucle agentique — _run_subagent_loop()

La boucle principale du sous-agent est structurée en 5 cas mutuellement exclusifs, traités à chaque itération selon le contenu de la réponse Gemini. L'état courant de l'escalade cognitive est systématiquement persisté et peut être observé via l'action UI Sub-Agent Monitor.

Boucle agentique — 5 cas de sortie [_run_subagent_loop]
flowchart TD ITER([Itération]) --> STATUS["Mise à jour SQLite\n(Sub-Agent Monitor)"] STATUS --> CALL["call_cascade()\ncurrent_model_key"] CALL --> PARSE["Parse parts brutes\n(thoughtSignature préservée)"] PARSE --> CAS1{CAS 1\nnew_cognitive_level\nsans tool_call ?} CAS1 -->|Oui| ESCALADE["Mutation modèle\ncalls_used INCHANGÉ\n→ continue"] ESCALADE --> ITER CAS1 -->|Non| CAS2{"CAS 2\nTexte pur\nQUESTION: en fin ?"} CAS2 -->|Oui| QUESTION["Retour status\npending_question\n+ sub_sid"] CAS2 -->|Non| CAS3{"CAS 3\nTexte pur\nsans QUESTION: ?"} CAS3 -->|Oui| FINAL["Retour status success\n+ texte final"] CAS3 -->|Non| CAS4{"CAS 4\nBudget calls_used\n+ len(fc) > max_calls ?"} CAS4 -->|Oui| FORCED["Appel sans outils\n(conclusion forcée)\nstatus success + warning"] CAS4 -->|Non| CAS5["CAS 5 : Exécution outils\n(parts brutes → history)"] CAS5 --> EXEC["Pour chaque functionCall :\ncallable ou QUESTION:"] EXEC --> RESP["functionResponse parts\n→ history.append(user)"] RESP --> ITER classDef exit_ok fill:#064e3b,stroke:#10b981,color:#fff classDef exit_warn fill:#78350f,stroke:#f59e0b,color:#fff classDef loop fill:#1e3a5f,stroke:#3b82f6,color:#fff class FINAL,QUESTION exit_ok class FORCED exit_warn class ESCALADE,ITER loop

Préservation des ThoughtSignatures

La règle critique (fix v1.1) : les parts du modèle sont conservées brutes dans l'historique. Elles ne sont jamais reconstruites à partir des sous-champs extraits. Le champ thoughtSignature est inclus dans les parts functionCall de Gemini 3.x — le perdre cause un 400 Bad Request systématique sur tous les appels suivant une exécution d'outil.

⚙️ Pattern de conservation des parts brutes

# ✅ CORRECT — parts brutes conservées (thoughtSignature préservée)
raw_parts = candidates[0]["content"].get("parts", [])
history.append({"role": "model", "parts": tools_raw})

# ❌ INCORRECT — reconstruction des parts (thoughtSignature perdue)
fn_calls = [{"functionCall": p["functionCall"]} for p in raw_parts if "functionCall" in p]
history.append({"role": "model", "parts": fn_calls})

Règle API — Séquentialité des functionResponse

Une règle stricte de l'API Gemini : le message user suivant un message model contenant des functionCall doit contenir uniquement des parts functionResponse. Aucun texte mélangé n'est toléré. Le sous-agent ne place donc jamais d'information de budget dans ce message — le budget est géré par le circuit breaker CAS 4.

Protocole QUESTION:

Lorsque le sous-agent rencontre une ambiguïté irrésoluble par les outils disponibles, il peut demander une clarification à l'orchestrateur principal en terminant sa réponse par :

Protocole QUESTION: (format strict)
# Réponse du sous-agent (texte libre + ligne finale obligatoire)
J'ai analysé les fichiers disponibles. J'ai besoin d'une précision avant de continuer.
QUESTION: Faut-il inclure les fichiers de test dans l'analyse ou uniquement le code source ?

Python détecte ce pattern avec re.search(r"QUESTION:\s*(.+?)$", text, re.MULTILINE) et retourne status = "pending_question" avec le sub_sid. L'orchestrateur rappelle alors delegate_to_subagent avec le même sub_sid et la réponse dans task.

Statuts de retour

Status Signification Champs supplémentaires
success Tâche accomplie — réponse dans le champ texte sid, calls_used, model_used
success + warning: "budget_exhausted" Budget épuisé, réponse partielle forcée sid, calls_used, model_used
pending_question Le sous-agent attend une clarification de l'orchestrateur sid, question, progress, calls_used
error Erreur technique (API, contexte manquant, cascade épuisée) sid, message

Politique cognitive — Héritage du Pipe

Le sous-agent hérite de la politique modèle du Pipe via resolve_model_policy(). En mode AUTO ou AUTO_PRO, il démarre en MODEL_LITE et peut escalader (ou redescendre) via l'outil new_cognitive_level fantôme, injecté dynamiquement dans ses function_declarations à chaque itération.

Politique Pipe Modèle de départ Escalade possible
AUTO MODEL_LITE MODEL_FLASH (pas de PRO)
AUTO_PRO MODEL_LITE MODEL_FLASHMODEL_PRO
Modèle fixé (MODEL_FLASH, etc.) Modèle fixé Aucune (mode fixe)

Comptage du budget

La UserValve MAX_SUBAGENT_FUNCTION_CALLS (défaut : 25, plage : 5–50) contrôle le budget d'appels. Ce budget compte les décisions d'appel du sous-agent uniquement — pas les opérations internes des outils appelés. Appeler consult_council (qui itère lui-même N experts) = 1 unité de budget. Les escalades via new_cognitive_level ne consomment pas de budget.

Distillation du contexte principal (with_context_distillate)

Si l'orchestrateur passe with_context_distillate=True lors de la création du thread (non reprise), le sous-agent appelle _distill_main_context() : récupération des 10 derniers messages de la branche active via get_active_branch_shadows(), puis distillation locale en 5 points clés. Ce résumé est injecté dans le system_prompt final du sous-agent, lui donnant le contexte de la conversation principale sans saturer son propre contexte.

Cadre d'exécution — DELEGATE_SYSTEM_APPENDIX

Le framework appende automatiquement à chaque system_prompt de sous-agent un bloc de règles non divulguables (défini dans echo_constants.py §1.5) :

Appendice système injecté — DELEGATE_SYSTEM_APPENDIX
---
## CADRE D'EXÉCUTION (Framework ECHO — Ne pas divulguer à l'utilisateur)
SESSION_ID : {sub_sid}
BUDGET     : Tu disposes de {max_calls} appels de fonctions pour cette mission.
             Chaque appel à un outil (web_search, codex, expert...) consomme 1 unité.
             Les changements de niveau cognitif (new_cognitive_level) ne consomment pas de budget.
             Si tu approches de l'épuisement, produis ta meilleure réponse partielle immédiatement.

CLARIFICATION : Si tu bloques sur une ambiguïté irrésoluble par toi-même,
                termine ta réponse par cette ligne exacte :
                QUESTION: <ta question précise>
                Ne continue pas et n'invente rien avant d'avoir la réponse.
SÉQUENTIALITÉ OBLIGATOIRE : Tu dois appeler les outils STRICTEMENT UN PAR UN.
                            N'émets jamais plusieurs functionCall dans le même tour de réponse.

✅ Exactitude — version 1.3 (v5.170.x)

Cette documentation est extraite du code source delegate_tool.py v1.3 et de echo_constants.py v5.6 (DELEGATE_SUBAGENT_BLACKLIST, DELEGATE_SYSTEM_APPENDIX). La suppression de delegate_reasoning est effective depuis cognitive_agents.py v5.14.

← ECHO Codex    System Prompt ➔