Accueil / Articles

Écrire les boucles que l’agent IA exécute dans une application mobile

Écrire les boucles que les agents IA exécutent dans une application mobile : quatre questions auxquelles chaque boucle doit répondre, six boucles imbriquées de la touche à la découpe, ainsi que des vérifications qui ne détectent rien.

Éditeur : REACTAPP.TOP9 min de lecture2122 mots

Sources: Loop Engineering on a Mobile App: What My AI Agent Runs, What It Reads, What Ends It, and When It Asks Me

Dites à un agent de se contenter de faire réussir le test de connexion, et il conçoit lui-même la boucle en temps réel. Deux choses se produisent généralement : soit il tente à nouveau jusqu’à ce que tout devienne vert, y compris un test qui vérifie désormais moins d’éléments qu’auparavant, soit il consulte son propre journal des événements, ne détecte aucune erreur et annonce que la tâche est terminée. Ces deux comportements constituent des boucles, et dans les deux cas le modèle choisit la conception en fonction de ce qu’il a lu par hasard.

L’ingénierie des boucles élimine ce choix de conception du modèle. Une personne décide à l’avance, par écrit, ce qui s’exécute, ce qu’elle lit pour prendre une décision, ce qui met fin à l’opération et quand il faut s’arrêter pour demander de l’aide à un humain. Ici, cela est appliqué à une application mobile.

La configuration

L’exemple concret présenté est une application de messagerie vocale pour Android et iOS, développée principalement à l’aide de Claude Code. Chaque session donne lieu à un dépôt Git distinct, appelé worktree. À l’intérieur de celui-ci, l’agent construit l’application, la installe sur un émulateur, gère l’interface utilisateur via Maestro – un exécuteur de tests en YAML qui lit l’arbre d’accèsibilité de l’écran (les contrôles étiquetés exposés par le système d’exploitation) – puis enregistre ses travaux. Rien dans les boucles ne dépend de cette application spécifique ; tout ce dont elles ont besoin, c’est d’un agent capable d’agir puis d’observer.

Le « harness » représente les outils, fichiers, appareils et règles qui déterminent ce que l’agent voit et fait. Les boucles, quant à elles, concernent un aspect différent : le comportement de l’agent au fil du temps avec ce « harness ».

Les quatre questions

Considérez le cycle le plus simple : appuyez sur un bouton, lisez l’arbre d’accessibilité et vérifiez si l’étiquette attendue est apparue. Sinon, attendez et relisez, puis échouez après dix secondes. Quelle que soit la taille d’un cycle dans une configuration d’agent, jusqu’à « cette fonctionnalité est-elle terminée ? », il doit répondre à quatre questions :

  1. Qu’est-ce qui s’exécute ?
  2. Que lit-il pour prendre une décision ?
  3. Qu’est-ce qui le termine ?
  4. Quand s’arrête-t-il et demande-t-il de l’aide à une personne ?

Sans les réponses aux quatre questions, il n’y a pas de cycle, seulement un script qui s’exécute une fois et espère. Répondre à ces questions pour chaque cycle exécuté par l’agent constitue toute la discipline ; le reste ne sont que des exemples.

Le cycle 3, connexion, montre les quatre réponses indiquées sur un assistant réel. Chaque flux de test dépendant d’un état commence par lui :

- launchApp:
    stopApp: true                    # 1. what runs: a cold launch, every time
- retry:
    maxRetries: 3                    # 3. what ends it: Home on screen, or three tries
    commands:
      - runFlow:
          when:
            visible:
              text: "Continue"       # 2. what it reads: the Login screen's button
          file: login.yaml           #    -> sign in only if it's showing
      - runFlow: assert-home.yaml    #    -> then prove Home is on screen
# 4. when it asks a human: never. A fourth failure is red, and red is the answer.

Une cause physique explique ce réessai. Immédiatement après un démarrage à froid, l’écran de chargement couvre à la fois l’écran de connexion et l’écran d’accueil pendant environ trois secondes, si bien que la première tentative ne montre rien des deux. La limite est de trois essais. Une simple instruction « réessayer jusqu’à ce que l’écran d’accueil apparaisse » ne s’arrêterait jamais en cas de build défectueux, tandis que trois essais permettent de gérer un démarrage lent et de faire apparaître en rouge une véritable panne.

Six boucles imbriquées

Les six boucles s’exécutent de la plus petite à la plus grande, chacune étant insérée dans la suivante. La boucle 1 s’exécute des dizaines de fois au sein de chaque test, chaque test se déroule dans le cadre d’un essai avec deux appareils, chaque essai fait partie des vérifications liées à un commit, et chaque commit fait partie d’une tâche spécifique.

Construisez d’abord la boucle la plus interne, car toutes les autres dépendent d’elle. Les boucles 1, 3 et 6 concernent le cycle de connexion, la mise en compte et la location des appareils. Les trois autres nécessitent une explication.

La boucle 2 correspond à une tentative de réessai limitée. Lorsque les caches sont vides, la première compilation dans un nouveau worktree peut planter au sein du compilateur Kotlin. Le fichier de mémoire de l’agent contient une règle en une seule ligne : tenter un réessai avant de soupçonner que le changement testé est problématique. Ce limite est importante. Un réessai illimité jusqu’à obtenir un résultat positif masquerait une véritable panne, tandis qu’un seul réessai transforme un problème connu en rien du tout et rend un deuxième échec significatif.

La boucle 4 est l’hook pré-commit. Il n’existe aucune CI dans ce dépôt, donc cet hook est la seule boucle qui s’active automatiquement à chaque modification. Il échoue de manière définitive : en l’absence du scanner de secrets gitleaks, le commit est refusé. Il exécute également les tests propres aux portes de validation avant celles-ci, car une porte incapable de trouver ses entrées affiche simplement « OK ». En résumé, il s’agit de :

#!/bin/sh
set -u
if ! command -v gitleaks >/dev/null 2>&1; then
    echo "pre-commit: gitleaks not found — refusing to commit without a secret scan." >&2
    exit 1
fi
gitleaks git --pre-commit --staged --redact --no-banner || exit 1

La seconde moitié est la suivante :

# The gates' own tests run first: a discovery bug produces no answer,
# after which every gate iterates an empty list and prints OK.
python3 ./scripts/test_gates.py >/dev/null 2>&1 || exit 1for gate in check_string_parity audit_hardcoded_strings; do
    [ -x "./scripts/$gate.sh" ] && { "./scripts/$gate.sh" || exit 1; }
done

La boucle 5 est la boucle externe qui entoure chaque unité de travail. Une tranche correspond à une petite partie d’une fonctionnalité pouvant être livrée, et elle passe par quatre étapes. Propose rédige le registre des modifications en indiquant ce qui change, pourquoi et les critères d’acceptation. Apply met ces modifications en œuvre. Verify vérifie le résultat par rapport à ce registre. Archive synchronise les spécifications et clôture la tranche. Verify constitue une étape à part entière avec des résultats distincts, et non une simple remarque de la part de l’implementateur indiquant que les tests ont réussi, afin que l’agent qui a écrit le code ne soit pas le seul à en garantir la qualité.

Des points de contrôle humains sont indiqués dans un tableau écrit montrant quelles décisions l’agent prend seul et lesquelles nécessitent d’abord une question. Une seule règle régit ce tableau : il faut d’abord se demander si inverser la modification entraînerait des coûts supérieurs à environ une journée de travail supplémentaire. Un agent qui saute une étape doit s’arrêter, expliquer son geste et obtenir l’approbation nécessaire.

La mémoire est ce qui transmet des informations entre les boucles et au cours des sessions : un ensemble de notes courtes que Claude Code relit chaque fois qu’une session commence. Ce projet en contient 52, chacune représentant une règle similaire à celle de la boucle 2. Sans elles, une nouvelle session répète le diagnostic précédent à partir de zéro.

Vérifications qui ne peuvent échouer

Toutes ces boucles se terminent lorsque une vérification indique "terminé". Une vérification qui indique "terminé" sans avoir détecté d’erreur brise silencieusement la boucle. Cela s’est produit cinq fois dans ce dépôt.

Une barrière qui ne pouvait pas échouer. Une barrière de localisation était un script qui bloquait une mise à jour lorsque du texte en anglais était codé directement dans l’interface utilisateur au lieu d’être lu depuis le catalogue de chaînes de caractères. Il contenait un filtre nommé filter_animation_labels, conçu pour éliminer uniquement les éléments ressemblant à des étiquettes d’animation. Le filtre testait window ~ tagbuilder à l’aide d’awk, mais aucun tagbuilder n’était jamais transmis à awk. Une variable awk non définie correspond à une chaîne vide, et window ~ "" correspond à tout, ce qui faisait que le filtre rejetait tous les éléments qu’il trouvait. Cette barrière n’avait jamais fonctionné une seule fois.

Le filtre a découvert 49 chaînes de caractères enregistrées directement dans 14 fichiers relevant de la fonction d’urgence de l’application, parmi lesquelles une contentDescription en anglais sur le bouton SOS, accompagnée d’une traduction inutilisée. Les captures d’écran effectuées sur le dispositif ont révélé tous les défauts de localisation réels dans cette zone, tandis que le mécanisme de contrôle n’en a détecté aucun. Faisant office de condition finale pour le boucle 4, il a examiné chaque modification pendant deux jours sans rien trouver.

Un mécanisme de contrôle qui ne fonctionnait pas. Les spécifications exigeaient que la compilation échoue, mais rien ne le garantissait en l’absence de processus d’intégration continue. La première version du script utilisait également exec gitleaks, ce qui remplace le shell et empêche ainsi l’exécution des lignes suivantes.

Un crochet qui fonctionnait avec tout. Un schéma if: peut restreindre les hooks de Claude Code à certains commandes. Un hook destiné uniquement à gh pr create utilisait Bash(gh pr create *), ce qui est trop restrictif. Les schémas ne contenant que le nom d’une commande échouent lorsqu’ils rencontrent $(), $VAR ou des heredocs. La solution était if: Bash(gh *), le script lui-même filtrant la commande lue depuis stdin.

Une suite qui ne testait rien. Pendant trois semaines, les flux de test pour iOS échouaient dès la première étape sans qu’aucun indicateur ne s’allume, car rien n’était exécuté. L’étape censée gérer le bouton SOS indiquait COMPLETED sans effectuer aucune action, de sorte qu’un test SOS donnait des résultats positifs identiques tant pour du code fonctionnel que pour du code défectueux.

Vérification valable pour le mauvais élément. Trois tests positifs concernaient un APK installé lors d’une autre session. Un validateur de spécifications a approuvé 182 des 182 spécifications, bien qu’une d’entre elles décrive une interface supprimée depuis des mois. Un PATCH a reçu la réponse 200 d’un serveur backend qui n’avait pas encore reçu la migration et a ignoré discrètement ce champ.

Au préalable, avant qu’une vérification ne gagne en fiabilité, il faut réintroduire exactement le défaut qu’elle est conçue pour détecter, vérifier que son indicateur devient rouge, puis supprimer ce défaut et s’assurer que l’indicateur redevient vert, tout cela dans la même session où la vérification a été écrite. Le script d’audit des documents utilisé ici suivait cette méthode : il changeait l’état, ajoutait un lien manquant ou supprimait une différence chaque fois que l’indicateur devenait rouge. Sa note de conception résume ce principe en une phrase : une vérification qui ne peut pas échouer n’a qu’une fonction décorative. Il ne conserve pas non plus de fichier de référence, car masquer 292 anomalies connues pour obtenir un résultat vert est interprété comme une couverture adéquate.

Où commencer

Commencez par écrire les quatre réponses avant tout code, sous forme de commentaire en haut du flux, du script ou de la règle. Une réponse manquante apparaîtra alors immédiatement, et non au sein d’une exécution qui se déroule silencieusement.

Vient ensuite le plus petit cycle, le cycle tap, qui présente deux exigences. Les contrôles doivent comporter des étiquettes : contentDescription ou testTag pour Android, accessibilityLabel pour iOS. De plus, l’agent a besoin d’une règle permanente pour chaque interaction avec l’écran. Ici, cette règle se trouve dans le fichier d’instructions du dépôt, celui que Claude Code charge au début de chaque session, et elle se résume à ceci :

Driving the emulator:
- Before every tap: `adb shell uiautomator dump`, and find the label you intend to tap.
- Tap the centre of that node's bounds. Never reuse a coordinate from an earlier dump.
- After the tap: dump again and confirm the expected label appeared. Poll up to 10 s,
  then stop and report red — do not guess a second tap.
- If the app won't launch: `pm dump <pkg> | grep enabled=` before suspecting the build.

Ces quatre phrases forment une boucle que l’agent exécute. Ce qui se passe, c’est un affichage, une lecture, puis un nouvel affichage. Il lit l’arborescence ; il s’arrête lorsque l’étiquette apparaît ou après 10 secondes. Il ne demande jamais à un humain, car le rouge est la réponse.

Finalement, avant de faire confiance à la vérification qui clôture la boucle, brisez-la délibérément. Renommez l’étiquette et voyez si la boucle devient rouge. Si elle reste verte, rien n’est lu.

Toute la configuration se résume à six éléments :

  1. Une boucle de lecture, décrite comme la règle ci-dessus dans le fichier d’instructions de l’agent. Tout le reste suppose que l’agent peut agir puis observer.
  2. Un crochet de pré-commit qui échoue, commité dans le répertoire et placé dans .git/hooks par un bref script de configuration, afin que chaque récupération exécute la version commitée. Sans analyseur, il n’y a pas de commit.
  • Un test de mutation par porte, effectué le jour même où elle est écrite : la présence d’un défaut donne du rouge, son absence donne du vert. Il doit être réalisé durant la même session, sinon cela n’aura jamais lieu.
  • Une tentative de réessai limitée pour chaque cas connu de défaillance, comme une compilation Gradle lente, le démarrage du simulateur ou l’écran d’accueil, enregistrée en une seule ligne dans la mémoire ou le fichier d’instructions : tenter une fois, puis imputer la faute au changement.
  • Une étape de vérification distincte de l’implémentation, avec des résultats écrits, c’est-à-dire une liste de contrôle par rapport au registre des changements et non une phrase indiquant que les tests passent.
  • Un point de contrôle humain en une phrase dans le fichier d’instructions : demander d’abord si inverser la décision coûterait plus d’un jour de travail supplémentaire.
  • Tout cela permet de créer un arbre de travail, un compte permettant de se connecter, une interface que l’agent peut lire, un dispositif utilisé pendant l’exécution, ainsi que des boucles qui s’arrêtent lorsque des vérifications révèlent un échec.