11 KiB
Audit — Point d'intégration agent pour le contrat finalize étendu
Date : 2026-05-20
Mission : Claude 4 (lecture seule)
Périmètre : agent_v0/agent_v1/network/streamer.py, agent_v0/agent_v1/main.py, agent_v0/agent_v1/ui/smart_tray.py, agent_v0/agent_v1/ui/chat_window.py, agent_v0/agent_v1/ui/shared_state.py, agent_v0/lea_ui/server_client.py
Contexte : le serveur va enrichir la réponse POST /api/v1/traces/stream/finalize avec un nouveau contrat { replay_ready, replay_request, launch_replay }. Objectif : identifier le plus petit point d'intégration côté agent pour consommer ce contrat proprement, sans remettre le VWB au centre.
Constat
Aujourd'hui, streamer.py:622-623 reçoit la réponse /finalize (result = resp.json()) et la jette dans un logger.info(f"Session finalisée: {result}"). Aucun consommateur ne voit ce payload. Tous les fils en aval (main.AgentV1.stop_session, smart_tray._on_stop_session, chat_window._on_quick_stop) terminent sans rien savoir du résultat serveur. La surface unique de capture du payload existe déjà — il suffit de l'exploiter.
Côté déclenchement replay, une seule surface client émet l'appel HTTP : smart_tray._launch_replay(workflow_id, workflow_name) (ligne 459-505) qui fait POST /api/v1/traces/stream/replay/start avec {"workflow_id": ...}. Tout le reste (chat_window, lea_ui/server_client) consomme un replay déjà initié (resume/abort, polling actions). Le nouveau contrat replay_request pourrait juste enrichir le payload passé à cet appel existant.
L'orchestration globale (AgentV1 dans main.py) wire déjà session start/stop via AgentState (shared_state.py). C'est le seul endroit qui voit à la fois le moment précis de la fin d'enregistrement (stop_session), l'identité de la session, et a accès à toutes les UI (tray + chat) via self._state.
Tableau — surfaces d'arrêt / relance et recommandations
Arrêt d'un enregistrement (où voir la fin)
| Fichier | Point d'entrée | Rôle | Risque branchement | Recommandation |
|---|---|---|---|---|
streamer.py:602-627 |
_finalize_session() |
Site unique qui reçoit resp.json() de /finalize |
Aucun — actuellement payload jeté | 🟢 POINT D'INTÉGRATION RECOMMANDÉ — ajouter callback on_finalize_result: Callable[[dict], None] invoqué après resp.ok. Découplé, non-bloquant, contenu. |
streamer.py:131-157 |
TraceStreamer.stop() |
Drain queue + appel _finalize_session |
Modifier le retour casse l'API publique (appelée par main.stop_session) |
🟡 Acceptable mais moins propre — retourner le payload via stop() couple les appelants à un retour qui n'existait pas. Préférer le callback. |
main.py:395-430 |
AgentV1.stop_session() |
Orchestre captor.stop + sleep + streamer.stop | Lieu naturel pour router le payload reçu — mais doit l'obtenir d'amont | 🟢 Destinataire idéal du callback — a accès à self._state pour notifier UI, et self.user_id pour reconstituer agent_<user_id> session de polling. |
shared_state.AgentState.stop_recording() |
Méthode | Source de vérité de l'arrêt UI | Étendre la signature _on_stop casse rétrocompat |
🟡 À éviter en première intention — l'état partagé doit rester muet sur le résultat serveur. |
smart_tray._on_stop_session() (ligne 396) |
Bouton tray "C'est terminé" | Notif "Merci ! J'ai bien mémorisé..." | Faire du HTTP ici dupliquerait _launch_replay |
🔴 À éviter — la couche UI ne doit pas appeler /finalize ni interpréter sa réponse. |
smart_tray._on_emergency_stop() (ligne 507) |
Bouton ARRÊT D'URGENCE | Stop immédiat sans finalize | Hors scope (arrêt brutal, pas de proposition replay) | ⚫ Aucun branchement — un emergency stop n'attend pas le replay. |
chat_window._on_quick_stop() (ligne 1458) |
Bouton chat "Arrêter" | Appelle shared_state.stop_recording() |
Idem ligne au-dessus | 🔴 À éviter — duplication. |
Relance d'un replay (où injecter replay_request / launch_replay)
| Fichier | Point d'entrée | Rôle | Risque branchement | Recommandation |
|---|---|---|---|---|
smart_tray._launch_replay(workflow_id, workflow_name) (ligne 459-505) |
Appel HTTP POST /replay/start |
Seul site qui déclenche un replay neuf côté client | Élargir la signature pour accepter un replay_request: dict (ou écraser workflow_id par tout le dict si fourni) |
🟢 Réutiliser tel quel — passer le replay_request du serveur comme JSON body. Surface existante, mature, notification Article 50 déjà cablée ligne 469-472. |
smart_tray._make_replay_callback() (ligne 326) |
Wrapping pour menu | Fabrique callbacks pour menu "Mes tâches" | Non touché par ce changement | ⚫ Aucun branchement |
main._replay_poll_loop() (ligne 276-345) |
Boucle permanente | Récepteur d'actions, pas déclencheur de replay | Auto-déclencher ici = invisible, non conforme Article 14 | 🔴 À éviter formellement — le poll loop reste polymorphique sur la session, ne doit pas démarrer de session. |
chat_window._on_paused_resume/_abort (ligne 1016-1080) |
Boutons bulle Pause | Resume/abort d'un replay déjà en cours | Hors scope (action sur replay existant) | ⚫ Aucun branchement |
lea_ui/server_client.LeaServerClient.start_polling() (ligne 261) |
API alternative | Polling parallèle non utilisé par main (qui a son propre loop) | Confusion : code dormant en partie | 🟡 Ne pas ajouter de logique ici — confirmer d'abord si ce client est encore actif au runtime. |
Notification / proposition utilisateur post-enregistrement
| Surface | Mécanisme dispo | Utilisation recommandée |
|---|---|---|
smart_tray._notifier.notify(title, msg) (NotificationManager) |
Notification système non-bloquante | 🟢 Notif simple "Tâche prête, voulez-vous la tester ?" si replay_ready=true mais launch_replay=false. |
smart_tray._ask_consent(title, msg) (ligne 74) |
Dialog tkinter Yes/No | 🟢 Si le serveur signale replay_ready=true, dialog post-enregistrement (réutilise le même pattern que le consentement avant enregistrement). |
chat_window (bulle paused) |
Tkinter custom bubble | 🟡 Réutiliser le pattern paused_bubble (ligne 902-1014) pour proposer un "Tester maintenant" / "Plus tard" serait propre mais plus de code. À garder pour une v2 produit. |
shared_state listeners |
Notification générique | 🟡 Émettre un nouvel événement type recording_finalized peut servir si plusieurs UI doivent réagir — mais ajoute du contrat. |
Trois options d'intégration
Option A — Solution minimale (recommandée)
TraceStreamerreçoit un setter optionnelset_on_finalize_result(callback: Callable[[dict], None]). Stocké en attribut.- Dans
_finalize_session()aprèsresp.ok, appel non-bloquantself.on_finalize_result(result)si défini. AgentV1.__init__wire le callback :self.streamer.set_on_finalize_result(self._on_finalize_result).AgentV1._on_finalize_result(payload):- si
payload.get("launch_replay") is TrueETreplay_requestprésent → invoqueself.ui._launch_replay(replay_request)(avec adaptation signature pour accepter un dict) - sinon si
payload.get("replay_ready") is True→self.ui._notifier.notify("Léa", "J'ai compris la tâche. Voulez-vous la tester ?")+ ouvre_ask_consent(en thread) - sinon : silencieux
- si
Volume estimé : ~15-20 lignes de code dans 2-3 fichiers. Aucun changement de contrat dans shared_state, chat_window, lea_ui. Réutilise _launch_replay et _ask_consent existants.
Conforme : Article 14 (contrôle humain) si dialog _ask_consent ; Article 50 (transparence) si _launch_replay est utilisé (notification déjà émise ligne 469-472).
Option B — Solution produit propre (à viser ensuite)
Tout de Option A, plus :
- Ajouter une bulle interactive dans
chat_window(pattern paused_bubble), au lieu/en plus du dialog tkinter, pour proposer le test avec contexte (nom de la tâche, durée, nombre d'actions). - Persister la proposition non répondue dans
AgentStatepour que l'utilisateur la retrouve même s'il a fermé la notif. - Logger côté audit_trail l'événement "test post-enregistrement proposé / accepté / refusé" pour conformité Article 12.
Volume estimé : +50-80 lignes, principalement UI chat_window. Plus de couplage shared_state.
Option C — À éviter
- Auto-déclencher le replay sans confirmation depuis le callback finalize, même si
launch_replay=true. Sans dialog, non conforme Article 14 (l'utilisateur peut ne pas être devant l'écran à ce moment). Si le serveur impose vraimentlaunch_replay=true, l'agent doit quand même afficher un compte à rebours visible (3-5s) avec annulation possible — mais pas exécuter silencieusement. - Faire le HTTP
/finalizedepuischat_windowousmart_traypour lire la réponse. Duplication, l'API serveur est déjà appelée par streamer. - Modifier
_replay_poll_looppour intercepter une transition "finalize→start replay". Couplage faux entre poll permanent et événement ponctuel ; race condition si l'utilisateur enchaîne plusieurs enregistrements. - Étendre la signature de
AgentState.stop_recording()pour propager le payload. La couche état partagé doit rester muette sur le contenu serveur. - Modifier
_finalize_session()pour retourner la valeur viastreamer.stop(). Casse l'API publique destop()(utilisée parmain.stop_session), oblige à modifier deux endroits, et empêche un fan-out à plusieurs listeners éventuels.
Conclusion
Un seul point d'intégration recommandé : ajouter un callback on_finalize_result à TraceStreamer, invoqué dans _finalize_session après réception de la réponse, wired par AgentV1 qui dispatche selon launch_replay / replay_ready vers les surfaces UI déjà existantes (smart_tray._launch_replay ou smart_tray._ask_consent + _notifier.notify).
Ce point :
- Capture la réponse là où elle existe déjà (
streamer.py:622) sans dupliquer d'appel HTTP. - Découpe la responsabilité : streamer = transport, main = orchestration, smart_tray = UI/consentement.
- Préserve le VWB hors-jeu : aucun appel ne transite par VWB, le replay est lancé directement par l'agent via l'endpoint
/replay/startdéjà éprouvé. - Respecte Article 14 si l'auto-lancement passe par
_ask_consentou une notif validable. - Coût d'implémentation : ~15-20 lignes de code, 2-3 fichiers.
Hors périmètre de cette mission : la définition exacte du contenu de replay_request (qui touche au workflow_id, à la machine cible, aux paramètres execution_mode). À spécifier côté serveur avant de figer la signature client.
Méthode d'audit
- Lecture intégrale :
shared_state.py(190 lignes),smart_tray.py(781 lignes),lea_ui/server_client.py(375 lignes). - Lecture déjà réalisée à l'audit Léa-first (2026-05-19) :
streamer.py(734 lignes),main.py(561 lignes). - Grep ciblé
chat_window.py(1619 lignes) :stop|replay|workflow|finalize|launch|on_stop|on_replay(60 résultats analysés). - Aucune modification de code, aucune exploration VWB, aucune refonte UI proposée — conformément aux interdits.