From c9b7cdabb71e4236ec6e5bd41085de31962d945f Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 23 Jun 2026 21:35:57 +0200 Subject: [PATCH] fix(core): signature de trajectoire stable malgre le moteur de grounding (by_text) Le champ by_role remontait la methode de detection (yolo/ocr/vlm), instable entre sessions : deux apprentissages du meme parcours detectes differemment produisaient deux signatures -> fusion (create-or-update) ratee. On sort by_role de la signature et on s'appuie sur le texte semantique de la cible (by_text), independant du moteur de grounding. Fallback quand by_text vide : titre de fenetre / description VLM. Test TDD: test_signature_stable_despite_grounding_role_difference (RED->GREEN). Co-Authored-By: Claude Opus 4.8 (1M context) --- core/execution/trajectory_signature.py | 14 +++++++++----- tests/unit/test_workflow_trajectory_signature.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/execution/trajectory_signature.py b/core/execution/trajectory_signature.py index e57c688c3..e4555c370 100644 --- a/core/execution/trajectory_signature.py +++ b/core/execution/trajectory_signature.py @@ -39,14 +39,18 @@ def trajectory_signature(steps: Iterable[Mapping[str, Any]]) -> str: # --------------------------------------------------------------------------- def _stable_target(target: Any) -> str: - """Descripteur de cible **stable** entre sessions (by_role/by_text, fallback window).""" + """Descripteur de cible **stable** entre sessions. + + S'appuie sur le texte sémantique de la cible (`by_text`), volontairement + indépendant du moteur de grounding : `by_role` peut valoir 'yolo'/'ocr'/'vlm' + (méthode de détection, instable entre sessions) et n'entre donc PAS dans la + signature. Fallback quand `by_text` est absent : titre de fenêtre / description VLM. + """ if not isinstance(target, Mapping): return "" - by_role = str(target.get("by_role") or "").strip() by_text = str(target.get("by_text") or "").strip() - base = f"{by_role}:{by_text}".strip(":") - if base: - return base + if by_text: + return by_text hints = target.get("context_hints") if isinstance(hints, Mapping): return str(hints.get("window_title") or hints.get("vlm_description") or "").strip() diff --git a/tests/unit/test_workflow_trajectory_signature.py b/tests/unit/test_workflow_trajectory_signature.py index 5e265bc6d..dd37b7105 100644 --- a/tests/unit/test_workflow_trajectory_signature.py +++ b/tests/unit/test_workflow_trajectory_signature.py @@ -69,3 +69,18 @@ def test_signature_follows_edge_chain_not_list_order(): scrambled = {"entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}, {"node_id": "n3"}], "edges": [e2, e1]} # liste inversée, même chaîne assert workflow_trajectory_signature(ordered) == workflow_trajectory_signature(scrambled) + + +def test_signature_stable_despite_grounding_role_difference(): + """`by_role` peut porter le moteur de grounding (yolo/ocr/vlm) — instable entre + sessions. La signature doit rester identique si seul `by_role` change → elle + s'appuie sur le texte sémantique `by_text`, pas sur la méthode de détection.""" + wf_yolo = { + "entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}], + "edges": [_edge("n1", "n2", "mouse_click", by_role="yolo", by_text="Fichier")], + } + wf_ocr = { + "entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}], + "edges": [_edge("n1", "n2", "mouse_click", by_role="ocr", by_text="Fichier")], + } + assert workflow_trajectory_signature(wf_yolo) == workflow_trajectory_signature(wf_ocr)