feat(workflow): action 'pause_for_human' — pause supervisée scriptée dans VWB

Nouvelle action native VWB qui force le replay à basculer en paused_need_help
avec un message custom. Quand Léa atteint cette étape, elle ne tente pas
d'exécuter — elle pose immédiatement le state, ce qui déclenche la bulle
interactive ChatWindow (J3.5) avec boutons Continuer / Annuler.

Asset démo majeur GHT Sud 95 : permet de scénariser le moment "Léa doute"
au bon endroit dans le workflow, sans dépendre d'un échec aléatoire.

Chaîne complète :
- VWB UI (types.ts) : nouvelle entrée ACTIONS catégorie 'logic', icône ⏸,
  paramètre 'message' éditable (textarea).
- Bridge VWB → core (learned_workflow_bridge.py) : passthrough du type +
  préservation du message dans parameters.
- Pipeline replay (replay_engine.py) : type ajouté à _ALLOWED_ACTION_TYPES,
  conversion edge → action normalisée préserve le message.
- Streaming server (api_stream.py /replay/next) : interception avant envoi
  à l'Agent V1 → bascule state en paused_need_help avec pause_message,
  retourne {action: None, replay_paused: True}.
- L'action n'est jamais transmise à l'Agent V1 — pure logique serveur.

10 nouveaux tests pytest. Total branche : 57/57 verts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-29 16:37:46 +02:00
parent 41c1250c99
commit 0e6e61f2b1
5 changed files with 177 additions and 1 deletions

View File

@@ -2853,6 +2853,35 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
# Peek à la prochaine action SANS la retirer (pour le pre-check)
action = queue[0]
# ── pause_for_human : interception avant exécution ──
# Cette action n'est jamais transmise à l'Agent V1. Elle bascule
# le replay en paused_need_help avec le message custom, ce qui
# déclenche la bulle interactive ChatWindow (J3.5).
if action.get("type") == "pause_for_human" and owning_replay is not None:
params = action.get("parameters") or {}
message = params.get("message") or "Validation requise"
queue.pop(0)
_replay_queues[session_id] = queue
owning_replay["status"] = "paused_need_help"
owning_replay["pause_message"] = message
owning_replay["failed_action"] = {
"action_id": action.get("action_id", ""),
"type": "pause_for_human",
"reason": "user_request",
}
logger.info(
f"Replay {owning_replay['replay_id']} pause supervisée demandée "
f"par le workflow : {message[:80]}"
)
return {
"action": None,
"session_id": session_id,
"machine_id": machine_id,
"replay_paused": True,
"pause_message": message,
"replay_id": owning_replay["replay_id"],
}
# ---- Pre-check écran (optionnel, non bloquant) ----
# Ne s'applique qu'aux actions qui ont un from_node (actions de workflow,
# pas les wait/retry auto-injectés ni les actions Copilot/Agent Libre)

View File

@@ -32,7 +32,8 @@ _ALLOWED_ACTION_TYPES = {
"click", "type", "key_combo", "scroll", "wait",
"file_open", "file_save", "file_close", "file_new", "file_dialog",
"double_click", "right_click", "drag",
"verify_screen", # Replay hybride : vérification visuelle entre groupes
"verify_screen", # Replay hybride : vérification visuelle entre groupes
"pause_for_human", # Pause supervisée explicite (interceptée par /replay/next)
}
_MAX_ACTION_TEXT_LENGTH = 10000
_MAX_KEYS_PER_COMBO = 10
@@ -852,6 +853,13 @@ def _edge_to_normalized_actions(edge, params: Dict[str, Any]) -> List[Dict[str,
keys = [action_params["key"]]
normalized["keys"] = keys
elif action_type == "pause_for_human":
normalized["type"] = "pause_for_human"
normalized["parameters"] = {
"message": action_params.get("message", "Validation requise"),
}
return [normalized] # pas de target/coords pour cette action logique
else:
logger.warning(f"Type d'action inconnu : {action_type}")
return []