diff --git a/agent_v0/server_v1/api_stream.py b/agent_v0/server_v1/api_stream.py index 403b4cdda..128620f10 100644 --- a/agent_v0/server_v1/api_stream.py +++ b/agent_v0/server_v1/api_stream.py @@ -3173,56 +3173,84 @@ async def report_action_result(report: ReplayResultReport): elif not report.success and agent_warning == "no_screen_change": # L'action a été exécutée mais l'écran n'a pas changé. # + # Philosophie Léa (feedback_failure_is_learning.md) : un échec + # n'est jamais un stop avec error — c'est un **moment pédagogique**. + # Léa demande à l'humain de montrer ce qu'elle aurait dû faire. + # # Comportement legacy (success_strict=False) : loguer l'échec # et continuer vers l'action suivante. Justifié pour les # workflows tolérants où un clic "sans effet" peut être normal # (ex: cliquer sur une case déjà cochée). # # Comportement strict (success_strict=True) : écran inchangé = - # vraie erreur. On déclenche un retry, puis stop après épuisement. - # Justifié pour les workflows critiques où chaque action doit - # produire un effet visuel observable. + # "je n'ai pas su faire". On redirige vers le mécanisme de pause + # supervisée existant (paused_need_help) pour que Léa demande à + # l'humain de montrer. Pas de retry automatique, pas de stop — + # on laisse la queue intacte et on attend l'intervention. _is_strict = False + _intent_strict = "" _idx_strict = replay_state.get("current_action_index", 0) _actions_meta_strict = replay_state.get("actions", []) if 0 <= _idx_strict < len(_actions_meta_strict): _current_strict = _actions_meta_strict[_idx_strict] or {} _is_strict = bool(_current_strict.get("success_strict", False)) + _intent_strict = str(_current_strict.get("intention", "") or "") - if _is_strict and retry_count < MAX_RETRIES_PER_ACTION: - # Strict + retries restants : on réinjecte l'action - logger.warning( - f"Action {action_id} STRICT : écran inchangé — " - f"retry {retry_count + 1}/{MAX_RETRIES_PER_ACTION}" + if _is_strict: + # Apprentissage supervisé : pause, demande d'intervention + _tspec = (original_action or {}).get("target_spec") or report.target_spec or {} + _target_desc = ( + _intent_strict + or _tspec.get("by_text", "") + or _tspec.get("vlm_description", "")[:80] + or "cette action" ) - _schedule_retry( - session_id, replay_state, - original_action or {"action_id": action_id}, - retry_count, "no_screen_change_strict", + replay_state["status"] = "paused_need_help" + replay_state["failed_action"] = { + "action_id": action_id, + "type": (original_action or {}).get("type", "unknown"), + "target_description": _target_desc, + "screenshot_b64": screenshot_after or report.screenshot, + "target_spec": _tspec, + "reason": "no_screen_change_strict", + "resolution_method": report.resolution_method or "", + "resolution_score": report.resolution_score or 0, + } + replay_state["pause_message"] = ( + f"Mon clic sur '{_target_desc}' n'a produit aucun effet. " + f"Peux-tu me montrer où je devais cliquer ?" ) - elif _is_strict: - # Strict + retries épuisés : échec définitif, stop replay - replay_state["failed_actions"] += 1 - replay_state["unverified_actions"] += 1 error_entry = { "action_id": action_id, - "error": "no_screen_change after strict retries", + "error": f"no_screen_change_strict: {_target_desc}", "retry_count": retry_count, "timestamp": time.time(), } replay_state["error_log"].append(error_entry) - replay_state["status"] = "error" - _replay_queues[session_id] = [] - logger.error( - f"Replay {replay_state['replay_id']} STRICT échoué : " - f"action {action_id} écran inchangé après {retry_count} retries" - ) - _notify_error_callback( - replay_state, action_id, - "no_screen_change after strict retries", + logger.warning( + f"Replay PAUSE supervisée (apprentissage) : {action_id} " + f"écran inchangé sur '{_target_desc}' — en attente " + f"d'intervention humaine" ) + # Logger l'échec pour l'apprentissage futur + try: + log_replay_failure( + replay_id=replay_state["replay_id"], + action_id=action_id, + target_spec=_tspec, + screenshot_b64=screenshot_after or report.screenshot, + error="no_screen_change_strict", + extra={ + "target_description": _target_desc, + "resolution_method": report.resolution_method or "", + "resolution_score": report.resolution_score or 0, + "actions_completed": replay_state["completed_actions"], + }, + ) + except Exception as _log_exc: + logger.debug("log_replay_failure skip: %s", _log_exc) else: - # Legacy : on continue + # Legacy (non-strict) : on continue, comportement historique replay_state["unverified_actions"] += 1 replay_state["completed_actions"] += 1 replay_state["current_action_index"] += 1