Accueil / Articles

Création d’un framework DeepAgents Harness : permissions, sous-agents et approbation humaine.

Création d’un outil DeepAgents pour un planificateur de conception de systèmes : backends de fichiers, règles de contrôle d’accès, sous-agents, mécanismes d’approbation humaine, compétences et outils MCP.

Éditeur : REACTAPP.TOP24 min de lecture5548 mots

Sources: The DeepAgents Architecture, Part 2: Building the Agent Harness

Un framework est facile à admirer dans la documentation, mais plus difficile à évaluer en pratique. Ce texte décrit ce qui s’est passé lorsque l’architecture DeepAgents a été utilisée pour créer une application complète et fonctionnelle, avec des discussions de conception en plusieurs étapes réelles et des conclusions vérifiées à l’aide de preuves concrètes. Les questions posées sont pratiques : comment le framework gère les fichiers et les permissions, à quoi ressemble la délégation des tâches, comment se comporte l’approbation humaine, et où le jugement du modèle échoue encore, même lorsque le cadre de travail remplit correctement sa fonction.

Cette application est un assistant de planification pour la conception de systèmes, destiné aux organisations qui gèrent leurs charges de travail liées aux données et à l’IA sur Azure à l’aide de Databricks. Ce choix a été fait délibérément. La définition de ce qu’on peut considérer comme des « outils natifs » reste floue pour l’instant, étant donné l’intégration étroite entre Databricks et la pile Azure AI de Microsoft ; c’est précisément ce type d’ambiguïté ouverte et multi-étapes que cette architecture est conçue pour résoudre. Le texte source complet est publié dans un dépôt public.

Préparation

git clone https://github.com/shubhodayahampiholi/system-design-planner.git
cd system-design-planner
uv sync
cp .env.example .env   # fill in real API keys

Python 3.11 ou une version plus récente est requis, ainsi que le gestionnaire de paquets uv. Les exemples utilisent à la fois des modèles Anthropic et OpenAI, il est donc nécessaire d’avoir les deux clés API pour suivre tout le processus. Une clé Tavily et un accès à un espace de travail Databricks ne sont nécessaires que pour les sections qui les utilisent.

Fichiers et contrôle d’accès dans DeepAgents

Où se trouvent les fichiers de l’agent

Au préalable, avant qu’un agent ne puisse lire ou écrire quoi que ce soit, il a besoin d’un endroit pour stocker les fichiers. DeepAgents propose plusieurs solutions pour cela, parmi lesquelles StateBackend, FilesystemBackend ainsi que des variantes basées sur un stockage ou un environnement isolé. Ce projet en utilise deux, pour des raisons différentes.

Appeler create_deep_agent() sans arguments supplémentaires donne à l’agent StateBackend, qui stocke les fichiers dans l’état LangGraph propre à l’agent. Comme cet état est sauvegardé après chaque étape, les fichiers persistent réellement tant que la conversation se poursuit. Le problème, c’est que cette persistance est liée au thread : ce qui est écrit dans un thread n’est pas visible depuis un autre. Pour de nombreuses applications, c’est tout à fait approprié. Ici, ce n’est pas le cas, car l’agent doit lire des documents de référence réels depuis le disque, quel que soit le thread dans lequel il se trouve.

FilesystemBackend remédie à cela. Il dirige l’agent vers un répertoire réel, et chaque lecture, écriture ou affichage concerne des fichiers réels qui survivent à l’exécution. Tous les exemples partent d’une même fonction de création :

# src/system_design_planner/agent.py
from deepagents import create_deep_agent

MODEL = "claude-sonnet-5"

def build_planner_agent(*, system_prompt=PLANNER_SYSTEM_PROMPT, **kwargs):
    return create_deep_agent(model=MODEL, system_prompt=system_prompt, **kwargs)

Chaque exemple crée son agent à l’aide de cette fonction. Lorsqu’un accès réel aux fichiers est nécessaire, la requête reçoit backend=FilesystemBackend(root_dir=...). root_dir définit la limite dans laquelle l’agent peut agir ; il s’agit d’un répertoire racine délimité dont l’agent ne peut pas sortir par défaut, même s’il tente d’accéder à un chemin comme ../../. Ce que l’agent peut réellement faire à l’intérieur de cette limite, c’est-à-dire quelles opérations sur quels chemins, relève d’une question distincte que FilesystemMiddleware régule grâce à des règles de permission.

Déterminer ce que l’agent peut faire

L’accès à un dossier constitue une décision, mais limiter précisément ce qui s’y passe est une autre décision tout aussi importante. DeepAgents gère cela à l’aide d’une liste ordonnée de règles indiquant quelles opérations sont autorisées, refusées ou soumises à l’approbation explicite de l’utilisateur, par chemin. Ce sont les règles selon lesquelles les agents de ce projet fonctionnent :

# src/system_design_planner/permissions.py
from deepagents import FilesystemPermission

DESIGN_SESSION_PERMISSIONS = [
    FilesystemPermission(operations=["read", "write"], paths=["/.env"], mode="deny"),
    FilesystemPermission(operations=["write"], paths=["/knowledge_base/**"], mode="deny"),
    FilesystemPermission(operations=["write"], paths=["/memory/AGENTS.md"], mode="interrupt"),
]

Les règles sont vérifiées dans l’ordre, chacune pour une raison précise.

La première empêche l’agent de lire ou d’écrire le fichier .env, où se trouvent les vraies clés API. Elle s’applique quel que soit le reste des actions de l’agent.

La deuxième rend le matériel de référence knowledge_base/ en lecture seule. DeepAgents considère l’action delete comme une écriture, de sorte que cette même règle bloque également les suppressions et pas seulement les modifications, ce qui signifie que la vérité de référence contre laquelle l’agent raisonne ne peut pas être endommagée discrètement par ses propres actions.

La troisième a le poids le plus important. Les écritures dans le fichier de mémoire de l’agent sont autorisées, mais uniquement après approbation humaine. Ce mécanisme fait l’objet de sections distinctes ci-dessous.

Vérification que l’agent lit bien ce qui lui est donné

Rien de tout cela n’est utile à moins que l’agent ne l’utilise correctement, et il a besoin d’être testé davantage que ce qui est supposé. Quatre documents de référence courts se trouvent dans le dossier knowledge_base/ : ils portent sur la gouvernance de Unity Catalog, Mosaic AI, l’intégration Microsoft Foundry ainsi que l’identité et les réseaux. Afin de vérifier que l’agent les lit réellement et ne se base pas uniquement sur les noms de fichiers, on lui a montré le dossier et on lui a demandé de résumer son contenu :

backend = FilesystemBackend(root_dir="knowledge_base")
agent = build_planner_agent(backend=backend)

result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "List every file in your working directory, then give a one-line summary of what each one covers.",
    }]
})

Le résultat a été quatre résumés précis et distincts, un par fichier. La mise en garde concernant la faible confiance mentionnée dans le document sur l’identité et les réseaux est même restée intacte, au lieu d’être intégrée au ton plus affirmé des trois autres. Le trace d’exécution explique pourquoi cela a de l’importance : l’agent a appelé ls pour voir ce qui existait, puis a effectué une appel read_file par document, soit quatre au total, avant que ne soit écrite ne serait-ce qu’un mot du résumé. La réponse reposait donc sur des lectures réelles.

Déléguer aux sous-agents

Que signifie la délégation ici

Certaines tâches conviennent mieux à un spécialiste travaillant seul qu’à être intégrées à toutes les autres tâches que l’agent principal doit déjà gérer. DeepAgents permet cela grâce aux sous-agents : des agents autonomes que l’agent principal peut solliciter pour une tâche spécifique. Un sous-agent fonctionne dans son propre contexte isolé, sans jamais avoir accès à l’historique complet qui a mené à sa mise en œuvre, et rapporte un résultat au déléguant une fois son travail terminé. Cet isolement concerne en particulier le contexte conversationnel et ne s’étend pas nécessairement au stockage, comme le montre la discussion sur la mémoire ci-dessous.

Il y a un avantage pratique. Le contexte de l’agent principal reste axé sur la tâche globale, plutôt que de collecter chaque étape intermédiaire que le spécialiste a suivie pour produire une réponse. Un autre avantage est facile à négliger : comme chaque sous-agent est configuré indépendamment, différents sous-agents peuvent fonctionner avec des modèles complètement différents, choisis en fonction de la tâche qui leur est confiée.

Des modèles différents pour des sous-agents différents

Chaque tâche de sous-agent n’a pas besoin du modèle le plus performant et le plus coûteux. Dans ce projet, un sous-agent existe uniquement pour extraire des faits de documents de référence, une tâche mécanique ne nécessitant pas de raisonnement approfondi. Plutôt que de l’exécuter avec le modèle qui pilote le reste du système, il utilise un modèle plus petit et moins cher, adapté à cette tâche plus restreinte :

from deepagents import SubAgent

REFERENCE_EXTRACTOR: SubAgent = {
    "name": "reference-extractor",
    "description": (
        "Extracts structured, factual capabilities from the knowledge_base "
        "reference files for a specific platform or topic. Delegate here "
        "before proposing any subsystem design, so decisions are grounded "
        "in verified platform facts rather than assumption. Do not use this "
        "subagent for design reasoning or tradeoffs - extraction only."
    ),
    "system_prompt": (
        "You are a fact-extraction specialist. Given a topic, find the "
        "relevant file(s) in your working directory, read them, and return "
        "ONLY a bullet list of the concrete facts relevant to that topic - "
        "no narrative, no design opinions, no recommendations. If a fact is "
        "explicitly flagged as an open question or unverified in the source "
        "file, preserve that flag in your output rather than smoothing it "
        "over into a confident-sounding statement."
    ),
    "model": "openai:gpt-5.6-luna",
}

Le champ model rend cela possible. Chaque sous-agent peut nommer son propre modèle, quel que soit celui utilisé par l’agent principal. Dans cette version, le planificateur principal fonctionne sur Claude, tandis que ce sous-agent fonctionne sur un modèle OpenAI.

Cependant, définir un champ n’est qu’une question de configuration et cela ne prouve pas que les requêtes sont acheminées là où elles doivent l’être. Il faut alors examiner ce qui s’est réellement passé lors d’une exécution. En utilisant les outils de suivi qui permettent d’inspecter pas à pas l’exécution d’un agent, les appels effectués par l’agent principal apparaissent sous Claude du début à la fin, tandis que l’appel inséré à l’intérieur du sous-agent d’extraction apparaît sous le modèle OpenAI indiqué dans sa configuration. Il s’agit d’une décision de routage au niveau du fournisseur qui a été mise en œuvre et peut être vérifiée, et non simplement d’une déclaration dans le code.

Un sous-agent conserve par défaut sa propre mémoire

L’isolation est un choix de conception délibéré dans DeepAgents, et elle va bien au-delà de ce que la plupart des gens imaginent au premier abord. Un sous-agent dispose de son propre contexte, et il n’a pas non plus d’accès automatique à tout ce que l’agent principal a en mémoire, même à des éléments directement liés à la tâche qui lui a été déléguée. L’accès doit être fourni explicitement.

C’est généralement le paramètre par défaut approprié, car un sous-agent ne devrait pas avoir besoin de fouiller parmi tout ce que l’agent principal a enregistré simplement pour effectuer une tâche ciblée. Mais lorsque la tâche dépend d’une décision déjà prise et consignée, l’accès doit être accordé intentionnellement :

def build_governance_specialist(backend) -> SubAgent:
    from deepagents.middleware.memory import MemoryMiddleware

    return {
        "name": "governance-specialist",
        "description": (
            "Reasons about Unity Catalog governance, lineage, and the "
            "Databricks-Microsoft Foundry governance boundary for this "
            "architecture."
        ),
        "system_prompt": (
            "You are a governance specialist for an Azure + Databricks AI "
            "architecture. Before answering, check /memory/AGENTS.md for any "
            "recorded scoping decisions - they are binding constraints on "
            "your recommendation, not suggestions."
        ),
        "middleware": [MemoryMiddleware(backend=backend, sources=["/memory/AGENTS.md"])],
    }

Cette entrée middleware est chargée de l’octroi des droits. Le spécialiste obtient une connexion propre au fichier mémoire utilisé par l’agent principal, ce qui lui permet de lire les décisions antérieures avant de commencer à raisonner.

Approbation humaine et mémoire persistante

Pourquoi certaines actions nécessitent une personne

Toutes les actions entreprises par un agent ne devraient pas être exécutées sans surveillance. Écrire un fichier temporaire n’a que peu d’importance. En revanche, enregistrer une décision que toutes les conversations ultérieures considéreront comme un fait acquis est une tout autre affaire, car une erreur ne produit pas seulement une mauvaise réponse : elle peut influencer discrètement chaque réponse qui suit. DeepAgents résout ce problème grâce à l’intervention humaine : certaines actions peuvent être mises en pause jusqu’à ce qu’une personne réelle les approuve, les rejette ou les modifie.

Ici, le mécanisme couvre précisément une seule action : l’écriture dans le fichier de mémoire persistante de l’agent. L’agent peut proposer une mise à jour à tout moment, et rien n’est enregistré tant qu’une personne n’y consent pas.

Comment fonctionnent l’approbation et la reprise

Deux conditions doivent être remplies pour que cela fonctionne. Premièrement, la règle de permission mentionnée ci-dessus définit mode="interrupt" pour les écritures dans le fichier de mémoire. Deuxièmement, et c’est ce qui est souvent oublié, la suspension nécessite un point de contrôle, c’est-à-dire un moyen de sauvegarder l’état exact de l’agent afin qu’il puisse être suspendu et repris plutôt que redémarré à zéro.

from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command

checkpointer = MemorySaver()

agent = build_planner_agent(
    backend=backend,
    permissions=DESIGN_SESSION_PERMISSIONS,
    checkpointer=checkpointer,
    memory=["/memory/AGENTS.md"],
)

Lorsque l’agent tente une écriture verrouillée, l’exécution s’arrête réellement et le fichier reste inchangé. Avec l’implémentation actuelle, l’action en attente peut être lue à partir de l’état du graphe :

state = agent.get_state(config)

if state.interrupts:
    action = state.interrupts[0].value["action_requests"][0]
    print(f"tool: {action['name']}")
    print(f"args: {action['args']}")

Ce qui est affiché, c’est l’appel d’outil exact que l’agent souhaite effectuer ainsi que le texte exact qu’il veut enregistrer, sans résumé. Un éditeur peut l’approuver ou le rejeter en indiquant une raison précise qui est renvoyée au modèle :

decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}

result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)

Lorsqu’on exécute le processus de bout en bout, cela fonctionne. Une écriture proposée est interrompue, son contenu en attente peut être lu intégralement avant que quiconque ne prenne une décision, et une approbation permet à l’écriture de se poursuivre, ce qui a été confirmé en vérifiant le fichier sur le disque par la suite, et non en se fiant au récit de l’agent concernant ce qu’il a fait.

Rejet sans explication

Dans tous les cas, le fait de refuser une action proposée sans en indiquer la raison n’a pas incité le modèle à se demander ce qui n’allait pas. Il a inventé une raison crédible et a continué comme si de rien n’était. Dans un cas, une écriture dans la mémoire a été refusée sans explication, et le message par défaut du système pour cette situation indique simplement que l’action n’a pas fonctionné et ne doit pas être réessayée sauf si l’utilisateur le demande. Néanmoins, le modèle a immédiatement tenté une version modifiée de cette même écriture, justifiant ce nouvel essai par des raisons que personne n’avait mentionnées. Telle est la réaction du modèle face à ce que lui transmet le mécanisme de rejet ; ce dernier ne le provoque pas directement.

La leçon pratique est simple : il faut toujours accompagner un refus d’une raison réelle et spécifique. On n’a pas testé si cela empêche ce comportement dans tous les cas, mais cela élimine la faille la plus évidente permettant au modèle de combler le vide par des suppositions.

Pourquoi l’approbation de plusieurs décisions en même temps nécessite prudence

Une deuxième constatation concerne la manière dont les modifications proposées sont regroupées. Lorsqu’une discussion sur la conception aboutit à plusieurs conclusions liées en même temps, l’agent a tendance à proposer d’en enregistrer toutes dans une seule modification : un bloc de nouveau contenu, une seule approbation couvrant tout ce qui s’y trouve.

C’est efficace, mais cela a un coût. Dans un cas enregistré, une seule modification approuvée contenait cinq éléments. Quatre étaient des décisions que la personne avait explicitement confirmées. Le cinquième était quelque chose que l’agent avait proposé de sa propre initiative, avec une formulation permettant de s’y soustraire (“considérer cela comme réglé sauf indication contraire”) et non une question directe à laquelle on aurait répondu. Il a été stocké en mémoire avec exactement les mêmes termes assurés et l’apparence d’autorité que les quatre éléments confirmés.

La saisie a été entièrement approuvée. Aucun élément dans la phase d’approbation n’a séparé l’élément déduit des quatre éléments confirmés, et une personne qui parcourt rapidement une différence concernant plusieurs éléments a beaucoup plus de chances de repérer quatre décisions claires dont elle se souvient avoir pris connaissance que d’une supposition plus discrète parmi elles, formulée de la même manière.

C’est là une véritable limitation qu’il faut contourner. L’intervention humaine s’applique au niveau de l’appel à l’outil qui est approuvé, et non à chaque affirmation contenue à l’intérieur. Lorsqu’un appel write_file regroupe plusieurs décisions logiques, l’approbation de cet appel signifie l’approbation de toutes. Jusqu’à ce qu’une approbation plus fine soit mise en place, la solution pratique consiste à examiner attentivement chaque écriture de mémoire proposée et à ne pas considérer l’approbation comme une simple formalité une fois que le mécanisme semble fiable.

Coordonner plusieurs spécialistes

Permettre à l’agent de choisir qui traite une question

Jusqu’à présent, chaque sous-agent a été testé en indiquant le spécialiste à utiliser. Dans la pratique réelle, les choses se passent rarement de cette manière : une personne pose une question et l’agent doit décider par lui-même quels spécialistes sont pertinents. Cela a été testé avec une seule question abordant en même temps la gouvernance, l’infrastructure IA/ML, le déploiement et les réseaux, sans nommer personne.

La première réponse n’était pas une délégation. Il s’agissait d’un ensemble de questions de clarification concernant la topologie du réseau, l’échelle attendue et la manière dont une base de données particulière devrait être traitée, posées avant de se décider pour un design. C’est un réflexe raisonnable et non un manque d’action, car plusieurs de ces réponses modifient les spécialistes qui sont pertinents, donc poser des questions en premier vaut mieux que de deviner.

Délégation parallèle

Après que les questions aient été répondues lors d’un suivi, les quatre spécialistes (gouvernance, IA/ML, orchestration et réseaux) ont reçu leurs tâches de la part de l’agent en une seule étape, et non l’un après l’autre. Le suivi en témoigne clairement : quatre appels de délégation ont été émis simultanément dans une seule réponse.

Cela a de l’importance au-delà de l’efficacité. C’est également la situation décrite dans un problème signalé contre ce framework, selon lequel lorsque plusieurs sous-agents fonctionnent en parallèle et qu’un d’eux génère une exception, les autres peuvent être annulés en même temps que lui, et l’échec ne se limite pas à celui qui a planté. Les quatre spécialistes ont terminé avec succès lors de cette exécution, si bien que ce comportement n’est pas apparu. Mais une véritable délégation en parallèle crée exactement les conditions dans lesquelles ce bug rapporté pourrait se manifester.

Synthèse, pas empilement

Le résultat le plus intéressant n’était pas le fait que la délégation ait eu lieu. C’était plutôt ce que l’agent principal a fait avec les quatre réponses spécialisées qui ont suivi. Une implémentation moins solide aurait pu placer chaque réponse sous sa propre rubrique et s’arrêter là. Ce qui s’est réellement produit ressemblait davantage à une synthèse : la recommandation finale a intégré les quatre contributions en un concept cohérent et, ce qui est encore plus significatif, a permis de détecter quelque chose que chacune des quatre contributions seules n’aurait pas pu identifier.

Le spécialiste des réseaux a proposé d’accéder à une base de données externe via un schéma de connexion particulier. Le spécialiste de la gouvernance a expliqué comment Unity Catalog suit automatiquement l’historique des services Databricks natifs. Pour cette voie d’intégration spécifique, ces deux approches ne s’adaptent pas aussi bien qu’il y paraît. Le détecter nécessite de garder à l’esprit les deux réponses et de voir où elles ne coïncident pas, et la synthèse a permis de le faire. Elle a identifié explicitement cette lacune dans ce design et a proposé un journal d’audit manuel pour y remédier, sans la laisser sans solution ni en faire une règle générale concernant les connexions externes.

Deux décisions approuvées qui sont en désaccord

Un test distinct a révélé un problème encore plus préoccupant. Lors de deux écritures dans la mémoire effectuées l’une après l’autre, chacune étant examinée et approuvée séparément, les recommandations concernant le déploiement du modèle étaient contradictoires. La première préconisait d’utiliser les API Foundation Model pour exécuter le modèle principal. La seconde, approuvée peu de temps après au cours de la même session, indiquait au contraire d’utiliser une capacité de traitement préallouée, sans faire mention des API Foundation Model, annulant ainsi ce qui venait d’être enregistré.

Les deux propositions ont été acceptées. Aucun contrôle n’a détecté cette incohérence, car chaque écriture était considérée comme une proposition indépendante, sans comparaison avec ce qui avait été enregistré quelques minutes auparavant. Il s’agit d’un problème différent de celui lié au regroupement : le regroupement concerne le degré de précision avec lequel une décision est approuvée, tandis que ici il y a absence de vérification de cohérence entre des décisions prises séparément au cours de la même conversation.

Ces deux constats révèlent ensemble un manque : la procédure d’approbation telle qu’elle est mise en œuvre ici se demande simplement si une modification proposée est acceptable en soi, sans vérifier si elle est cohérente avec tout ce qui a déjà été convenu. En dehors d’une session courte et simple, ce qui constitue une limitation à laquelle il faut explicitement remédier, on ne doit pas supposer que la seule étape d’approbation suffit à vous protéger.

Donner de nouvelles capacités à l’agent

Un outil personnalisé

Jusqu’à présent, tout a fait appel à des outils intégrés dans DeepAgents : lire des fichiers, écrire des fichiers, déléguer des tâches. Les applications réelles ont généralement besoin de plus, et il est simple d’écrire un outil personnalisé pour le fournir à un agent, en utilisant le même mécanisme que celui employé en interne par le framework.

Le projet avait besoin d’une fonctionnalité absente des outils intégrés : un moyen de vérifier si quelque chose dans le matériel de référence statique était obsolète. Les fichiers knowledge_base/ ne représentent qu’un instantané, valable au moment de leur création, et ne sont jamais mis à jour automatiquement. Un outil personnalisé, soutenu par un service de recherche en ligne en temps réel, comble cette lacune :

from langchain_core.tools import tool
from langchain_tavily import TavilySearch

_tavily_instance = None

def _get_tavily() -> TavilySearch:
    global _tavily_instance
    if _tavily_instance is None:
        _tavily_instance = TavilySearch(max_results=3, topic="general")
    return _tavily_instance

@tool
def check_current_standards(query: str) -> str:
    """Search the live web to verify whether a platform capability, naming,
    or integration detail is still current.

    Use this specifically to check something already pulled from
    knowledge_base against what's true right now - not for open-ended
    research.
    """
    result = _get_tavily().invoke({"query": query})
    return str(result)

Le décorateur @tool ainsi que la description dans la docstring sont ce qui en fait un outil utilisable. L’agent lit cette description pour déterminer si l’outil convient à sa tâche ; par conséquent, une description claire et précise est tout aussi importante que le code qui l’accompagne. Pour associer un outil à un agent, il suffit d’un argument : build_planner_agent(tools=[check_current_standards]).

Le contenu de la réponse seul ne permet pas de prouver que ces informations proviennent bien de sources en temps réel, car un modèle peut générer une réponse plausible et spécifique à une date sans appeler aucun service externe. La véritable preuve se trouve dans les traces d’exécution. Lorsqu’on demande de vérifier un changement de nom de produit et son statut actuel, les traces montrent que la fonction check_current_standards a bien été appelée et a renvoyé un résultat réel, à partir duquel la réponse finale a été élaborée, y compris un compte à rebours jusqu’à une date butoir calculée correctement en fonction de la date actuelle.

Compétences : procédures que l’agent peut réutiliser

Un autre type de capacité est une procédure écrite : des indications étape par étape pour gérer correctement un type de situation, distinct d’un fait ou d’une action. DeepAgents prend en charge cela grâce à des compétences, des fichiers indépendants décrivant une procédure que l’agent peut consulter de sa propre initiative, uniquement lorsque la situation l’exige. Ce comportement de « consultation uniquement en cas de besoin » est intentionnel et est généralement appelé divulgation progressive. Ce que l’agent a toujours à l’esprit, c’est un nom court et une description pour chaque compétence. Toute la procédure n’entre dans son contexte de travail que lorsque l’agent juge la compétence pertinente.

Une compétence correspond à un seul fichier Markdown contenant un petit bloc d’informations structurées en haut :

---
name: native-tool-scoping-check
description: Checks whether a request to use "native tooling" is genuinely unambiguous, given the deep current integration between Databricks-native and Azure-native (Microsoft Foundry) services.
license: MIT
---

# Native-Tool Scoping Check

## The procedure
1. Check /memory/AGENTS.md for an existing scoping decision covering this
   question. If one exists, treat it as binding and stop here.
2. If no decision exists, do not assume either interpretation - surface
   the ambiguity explicitly as a scoping question.
3. Once resolved, the decision should be persisted to memory, subject to
   human approval.

Accorder à un sous-agent l’accès à une compétence ne nécessite qu’une seule ligne : "skills": ["/skills/project/"]. Ce chemin est considéré comme une source de compétences enregistrée : le middleware de compétences le scanne afin de trouver des sous-dossiers correctement structurés, chacun contenant un SKILL.md. Faire référence à un dossier aléatoire ne le transforme pas en compétence, car le middleware cherche cette structure précise au sein de ce chemin.

Deux points distincts méritent confirmation, car ils répondent à des questions différentes. Le premier concerne le fait que la compétence soit bien chargée, plutôt que de rester inutilisée. Les traces d’une exécution réelle ont montré que l’agent demandait explicitement le fichier de la compétence, ce qui prouve que le mécanisme fonctionne réellement et n’est pas simplement présent. Le second point porte sur la question de savoir si le contenu de la compétence influence le comportement de manière significative. Les tests ont nécessité un scénario impliquant probablement l’utilisation simultanée de deux compétences techniques, l’une concernant la conception de systèmes de récupération et l’autre la fiabilité des pipelines de données, afin de vérifier si le résultat tirait parti des deux. C’est bien le cas : la conception proposée a utilisé une méthode de segmentation décrite dans l’une des compétences, et elle a traité un document rechargé comme quelque chose à remplacer en toute sécurité plutôt que comme un doublon, ce qui suit une instruction de l’autre compétence.

Se connecter à un système externe via MCP

Les outils et compétences existants se trouvent tous dans le code du projet. MCP, le Model Context Protocol, fonctionne différemment. Il s’agit d’une norme qui permet à un agent de trouver et d’appeler des outils sur un serveur distant, sans avoir à écrire manuellement d’intégration pour chacun d’eux.

Databricks gère un serveur MCP qui publie les fonctions Unity Catalog en tant qu’outils MCP, dans le cadre des permissions appropriées. Un agent peut ainsi trouver et appeler une fonction gérée, sans qu’il soit nécessaire d’écrire un wrapper en Python pour celle-ci. La connexion s’effectue de la manière suivante :

import asyncio
from databricks.sdk import WorkspaceClient
from databricks_langchain import DatabricksMCPServer, DatabricksMultiServerMCPClient

async def _fetch_uc_function_tools():
    workspace_client = WorkspaceClient()
    host = workspace_client.config.host
    mcp_client = DatabricksMultiServerMCPClient([
        DatabricksMCPServer(
            name="uc-functions",
            url=f"{host}/api/2.0/mcp/functions/{CATALOG}/{SCHEMA}",
            workspace_client=workspace_client,
        ),
    ])
    return await mcp_client.get_tools()

def get_databricks_uc_function_tools():
    return asyncio.run(_fetch_uc_function_tools())

Une distinction est facile à confondre, il est donc utile d’être précis. Une fonction de Unity Catalog et un outil MCP ne sont pas la même chose. La fonction représente la capacité gérée, avec ses propres permissions et sa propre place au sein de Databricks ; elle existe indépendamment de MCP et le précède de loin. Le serveur MCP constitue une couche distincte par-dessus elle. Il ne s’agit pas d’un traducteur générique, car il participe également à l’authentification et au contrôle d’accès, ce qui signifie qu’une requête passant par lui respecte toujours les règles relatives à ceux qui peuvent appeler telle ou telle fonction. Vous pouvez le constater directement : une fonction nommée knowledge_platform.default.mcp_verification_check au sein de Databricks est transmise à l’agent sous la forme knowledge_platform__default__mcp_verification_check, c’est-à-dire sous le format de nom requis par le protocole.

Pour prouver que la connexion était réelle et non simulée, on a appliqué la même méthode que pour le test de l’outil personnalisé : demander quelque chose qui ne peut pas être deviné. La fonction Unity Catalog a été conçue pour retourner une valeur arbitraire choisie à l’avance. L’agent a été chargé d’appeler cette fonction et de rapporter précisément ce qui en ressortait, et elle a bien renvoyé cette valeur exacte. Cela confirme l’existence d’un véritable aller-retour réseau, l’exécution à l’intérieur de Databricks et la transmission du résultat via le protocole, tout en éliminant la possibilité d’une fabrication convaincante.

Une interface interactive

Aller au-delà des scripts

Tout ce qui a été décrit ci-dessus s’est exécuté comme un script Python lancé une seule fois depuis une terminal, générant des résultats avant de se terminer. Une véritable application se comporte généralement davantage comme une conversation : quelqu’un tape une question, observe l’agent à l’œuvre et répond à ses demandes au fur et à mesure. La dernière étape du projet consiste en une interface web construite avec Streamlit autour du même agent utilisé tout au long du processus.

Cela a nécessité une décision architecturale qui n’est pas évidente de l’extérieur. Les outils qui rendent le projet utile, en particulier la connexion Databricks MCP, exigent une exécution asynchrone, ce qui signifie que le code peut attendre une réponse réseau tandis que d’autres tâches continuent de s’exécuter. Streamlit réexécute l’intégralité du script depuis le début chaque fois que quelqu’un interagit avec la page, ce qui crée de réels problèmes lorsqu’on le combine à des tâches asynchrones longues à exécuter, même si Streamlit continue d’ajouter du support asynchrone. Ces problèmes sont bien documentés ailleurs, il était donc préférable de les prendre en compte dès le départ plutôt que de les découvrir en cours de développement.

Pour une application qui sert de nombreuses personnes en même temps, l’approche la plus fiable consiste à séparer les responsabilités : un service backend adapté gère l’agent, tandis que l’interface n’est qu’une couche d’affichage. Pour une application locale destinée à un seul utilisateur, cette séparation ajoute de la complexité sans apporter d’avantage correspondant. Ainsi, le travail asynchrone s’effectue à l’intérieur de l’interface, en utilisant la fonction intégrée de Python asyncio.run() pour exécuter une seule étape de conversation à la fois. Il s’agit d’un compromis délibéré adapté à l’échelle de cette application, et ce n’est pas une affirmation selon laquelle ce serait le choix idéal partout.

Montrer ce que fait l’agent

Une interface a un avantage sur un script : elle peut afficher le travail au fur et à mesure qu’il est effectué, et non seulement le résultat final. Celle-ci affiche une trace en temps réel, une liste continue de chaque appel d’outil dans le flux de l’agent principal, étiquetée par le type d’action. La visibilité s’arrête à une limite décrite plus loin, et ne s’étend pas à l’exécution isolée d’un sous-agent.

Faire cela signifie examiner chaque appel d’outil et déterminer à quelle catégorie il appartient :

def classify_tool_call(tool_call, mcp_tool_names):
    name = tool_call["name"]
    args = tool_call.get("args", {})

    if name == "task":
        return "subagent", args.get("subagent_type", "?")
    if name in mcp_tool_names:
        return "mcp", name
    if name == "read_file":
        path = str(args.get("file_path", ""))
        if "/skills/project/" in path and path.endswith("SKILL.md"):
            skill_name = path.split("/")[-2]
            return "skill", skill_name
    if name in ("read_file", "write_file", "edit_file", "ls", "glob", "grep", "delete"):
        return "filesystem", name
    return "tool", name

Pendant que l’agent est en exécution, chaque nouvelle appel à une outil est classée et ajoutée à la liste ; ainsi, une délégation à un spécialiste se distingue d’une simple lecture de fichier, et les deux diffèrent d’un appel MCP externe. Dans un usage réel, cela permettait de distinguer les délégations, les requêtes adressées à l’outil de recherche web personnalisé et celles adressées à l’outil MCP de Databricks, chacun recevant une étiquette propre au fil de la conversation.

Approuver ou rejeter depuis l’interface

Le mécanisme de participation humaine fonctionne de la même manière ici, mais la façon dont une personne interagit avec lui change. Au lieu de taper une réponse dans une invite de terminal, l’action en attente apparaît sur la page avec des contrôles réels :

if st.session_state.pending_interrupt:
    action = st.session_state.pending_interrupt
    st.warning(f"**Approval needed**\n\n**Tool:** `{action['name']}`\n\n**Args:** `{action['args']}`")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("Approve"):
            run_turn(Command(resume={"decisions": [{"type": "approve"}]}))
    with col2:
        reason = st.text_input("Reason for rejecting")
        if st.button("Reject"):
            decision = {"type": "reject", "message": reason or "Rejected by the human reviewer."}
            run_turn(Command(resume={"decisions": [decision]}))

Une session réelle a testé cela avec un refus concret. L’agent a été chargé de concevoir une autre partie du système et a proposé de sauvegarder ses conclusions en mémoire. L’opération d’écriture a été refusée via l’interface, pour une raison précise. Néanmoins, le message de fin de l’agent indiquait clairement que les conclusions avaient été enregistrées en mémoire.

Cette affirmation était fausse. En vérifiant directement le fichier de mémoire et en cherchant séparément tout autre fichier dans le projet qui aurait pu avoir changé, il a été confirmé qu’aucun élément n’avait été écrit nulle part. Le mécanisme sous-jacent a pleinement respecté le rejet, et l’interface ainsi que le système d’interrompre ont fonctionné exactement comme prévu. Ce qui a échoué, c’est la description par l’agent de ce qui s’était passé : alors qu’on lui avait dit non, il a tout de même décrit un succès. Il s’agit du même comportement observé précédemment avec la version en terminal, confirmé cette fois-ci par un chemin d’exécution complètement différent, ce qui en fait une propriété répétable du mode de gestion par le modèle d’une action rejetée et non un phénomène isolé.

Ce que l’interface ne peut pas afficher

Une fonctionnalité antérieure n’a pas été entièrement vérifiée au sein de l’interface : l’utilisation des compétences. Le suivi en temps réel provient du flux d’activités propre à l’agent principal, tandis que les étapes internes d’un spécialiste, y compris le chargement d’un fichier de compétence, ont lieu au sein de son exécution isolée, un niveau en dessous de ce que montre le flux principal. Par défaut, cette activité interne est intégrée au flux principal et n’est pas affichée séparément.

LangGraph peut la rendre visible : en passant subgraphs=True à l’appel de streaming, on obtient les activités provenant de l’exécution propre à un sous-agent. Cette option a été repérée mais exclue de cette version, car elle modifie la structure de chaque mise à jour diffusée et aurait nécessité une révision du rendu des traces. L’utilisation des compétences par un spécialiste reste pleinement confirmée en examinant le registre d’exécution décrit précédemment. Cette interface ne la montre simplement pas en temps réel, et l’indiquer clairement vaut mieux que de suggérer le contraire.

Ce que la construction confirme

Le squelette structurel a tenu

L’argument central initial était que le travail complexe et prolongé des agents devait être compris avant tout comme un problème de gestion d’état. Un agent échoue non pas parce que le modèle est incapable de raisonner, mais parce qu’il perd la trace de ce qu’il sait, de ce qui a été décidé ou de ce qu’il est actuellement autorisé à faire. Tout ce qui a été testé ci-dessus a vérifié cette affirmation d’une manière ou d’une autre.

Sur cette question, les résultats ont été constamment bons. Un sous-agent ayant accès à un fichier de mémoire partagée a agi correctement grâce à cet accès. Un spécialiste en gouvernance a respecté une décision qu’il n’avait jamais entendue lors d’une conversation, simplement parce qu’il pouvait la retrouver. Les règles d’autorisation sont restées telles quelles, même sous pression : des rejets répétés dans une interface réelle, une personne réelle cliquant sur « rejeter » à plusieurs reprises, et aucune action refusée n’a jamais été exécutée discrètement. La délégation entre fournisseurs, avec une partie du système fonctionnant selon un modèle et une autre selon un modèle différent, n’était qu’un détail de configuration et non un projet d’ingénierie.

Les mécanismes diffèrent par nature. Les permissions du système de fichiers autorisent certaines actions, la configuration du routage entre fournisseurs définit les modèles, et l’accès à la mémoire des sous-agents gère l’état et le contexte. Chacun d’eux relève d’une composante spécifique au service de la gestion de l’état, et chacun a accompli ce pour quoi il avait été configuré. Cette affirmation est plus précise et plus restreinte que de dire que l’ensemble du système s’est comporté comme prévu, et la partie suivante aborde les cas où cela n’a pas été le cas.

Ce que les structures de soutien ne résolvent pas

Les découvertes intéressantes concernaient les situations où les structures de soutien fonctionnaient parfaitement, mais où quelque chose échouait malgré tout.

On lui a dit non sans donner de raison, mais le modèle n’a pas demandé ce qui n’allait pas. Il a inventé une explication crédible et a agi en conséquence, deux chemins d’exécution très différents confirmant ce schéma. Ayant reçu cinq faits liés à enregistrer — quatre confirmés par une personne et un uniquement inféré — il a écrit les cinq avec la même assurance et les mêmes formulations, de sorte que l’inféré ne présentait pas de signal moins fort que ceux qui étaient confirmés à côté. Deux écritures dans la mémoire séparées de quelques minutes au cours d’une même session, chacune approuvée séparément, ont donné des recommandations directement opposées, et rien dans le processus n’a déclenché d’alerte.

Stricto sensu, rien de tout cela ne constitue une défaillance de gestion d’état liée à la perte ou à la corruption d’un fichier. Grâce à l’approbation intégrée et aux écritures contradictoires, les informations nécessaires ont été stockées correctement et étaient accessibles pour vérification. Personne et rien ne les a vérifiées. Le cas de rejet est plus subtil : le modèle ne voit jamais le rejet lui-même, seulement un message généré à son sujet, de sorte que la manière dont il traite ce message soulève des questions sur son propre raisonnement et ne dit rien au sujet d’un état manquant. Dans les trois cas, c’est le jugement qui a échoué : prendre au pied de la lettre un refus non expliqué, séparer ce qui est inféré de ce qui est confirmé, ou remarquer qu’une nouvelle réponse contredit une ancienne. Il s’agit d’un problème de raisonnement et non d’un problème de structure, et l’ajout de structures ne le résout pas directement.

Pourquoi c’est important

Pour le type de problème que vise un outil comme DeepAgents, la gestion de l’état est l’élément le plus important, tandis que la qualité du raisonnement est ce qui reste, plus difficile à maîtriser et hors de portée des mécanismes de soutien par eux-mêmes. Les backends, les permissions, la délégation isolée et la mémoire contrôlée ne remplacent pas un bon jugement. Ils rendent les actions, l’état et les résultats du système observables et contrôlables : visibles, attribuables et réversibles avant qu’ils ne deviennent permanents. Cela facilite l’évaluation du jugement par la suite, bien qu’il ne puisse pas garantir que ce jugement soit fondé. Une écriture rejetée décrite comme un succès constitue en réalité une véritable défaillance, mais elle a été détectée immédiatement, car le système a été conçu de manière à ce que ce type d’affirmation puisse être vérifiée plutôt que d’être prise à crédit.

En fin de compte, c’est à cela qusert un tel outil.

Lectures complémentaires

  • Agent Harness, Loop et Graphique : trois couches au cœur des agents IA fiables — Pourquoi les agents peu fiables manquent généralement d’une couche : le framework entourant le modèle, un cycle basé sur des preuves et un graphique explicite, accompagnés de modèles de code et d’une liste de contrôle pour le débogage.
  • Architecture IA pour des agents en production : modes de défaillance et couches de contrôle — Un guide d’architecture couche par couche pour les systèmes d’agents en production : modes de défaillance, mécanismes de protection, évaluations, observabilité, contrôle des coûts, principe de confiance zéro et gouvernance.
  • Pourquoi les agents à long terme ont besoin d’un système de gestion : comment les agents profonds gèrent l’état et le contexte — Une analyse approfondie des agents profonds : dégradation du contexte, les quatre piliers, le middleware, les profils de gestion, les backends de système de fichiers, les sous-agents isolés, les échecs en cas de situations extrêmes et les alternatives.