From e4058fd8119b72d0b06289031f08bab8e89cba66 Mon Sep 17 00:00:00 2001 From: Rahul Ranjan <11793455+rahul05ranjan@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:23:45 +0530 Subject: [PATCH] fix(agent): accept the curly apostrophe in click-negation replies (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit unmade_browser_click_note spelled every contraction with the ASCII apostrophe, so a true negated reply typed with the curly one (wasn't with U+2019 — qwen3:8b does this) fell through to the passive "clicked" claim and got a redundant correction. Normalize U+2018/U+2019 to the ASCII apostrophe before matching, which covers the negation, assistant and hypothetical patterns at once. Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- changes/498.fixed.md | 1 + sentient/agent/outcomes.py | 6 +++++- tests/test_agent_loop.py | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changes/498.fixed.md diff --git a/changes/498.fixed.md b/changes/498.fixed.md new file mode 100644 index 000000000..58b81b0f8 --- /dev/null +++ b/changes/498.fixed.md @@ -0,0 +1 @@ +**A reply that says it didn’t click something is believed when the apostrophe is curly.** Before, small-model replies that wrote wasn’t or didn’t with a typographic apostrophe were read as positive click claims and got a redundant correction. diff --git a/sentient/agent/outcomes.py b/sentient/agent/outcomes.py index b23160ba7..22f241332 100644 --- a/sentient/agent/outcomes.py +++ b/sentient/agent/outcomes.py @@ -395,7 +395,11 @@ def unmade_browser_click_note(text: str, calls: list[dict], history: list[dict] """ current_clicks = [call for call in calls if call.get("name") == "browser_click"] previous_clicks = [call for call in history or [] if call.get("name") == "browser_click"] - claims = re.split(r"(?<=[.!?])\s+", text or "") + # Small models often type the curly apostrophe (wasn\u2019t), while the + # claim patterns spell contractions with the ASCII one; normalize first + # (#498), so a negated claim is never read as a positive one. + text = re.sub("[\u2018\u2019]", "'", text or "") + claims = re.split(r"(?<=[.!?])\s+", text) # A reply can summarize a successful click from an earlier turn alongside a # declined click from this one, so consider the complete chronological record. clicks = previous_clicks + current_clicks diff --git a/tests/test_agent_loop.py b/tests/test_agent_loop.py index e8ba42057..e911b6c9b 100644 --- a/tests/test_agent_loop.py +++ b/tests/test_agent_loop.py @@ -238,6 +238,26 @@ def test_browser_click_correction_uses_the_latest_call(): assert outcomes.unmade_browser_click_note( "You clicked A, and I clicked B.", [] ) == "I didn't click it, so nothing changed on the page." + + +def test_browser_click_negations_accept_the_curly_apostrophe(): + """Small models often type wasn\u2019t; a true negated reply must not be corrected (#498).""" + declined = [{"name": "browser_click", "declined": True, "target": 'button "Subscribe"'}] + for negated in ( + "No, the Subscribe button wasn\u2019t clicked.", + "I didn\u2019t click the Subscribe button.", + "I haven\u2019t clicked the Subscribe button.", + "The Subscribe button hasn\u2019t been clicked yet.", + "It wasn\u2019t clicked, the approval was declined.", + ): + assert outcomes.unmade_browser_click_note(negated, declined) == "", negated + assert outcomes.unmade_browser_click_note(negated, []) == "", negated + # the straight-apostrophe spellings keep working, and a curly positive + # claim is still corrected + assert outcomes.unmade_browser_click_note("No, the Subscribe button wasn't clicked.", declined) == "" + assert outcomes.unmade_browser_click_note( + "I\u2019ve clicked the Subscribe button.", declined + ) == "I didn't click it: you declined, so nothing changed on the page." prior_success = [{"name": "browser_click", "ok": True}] assert outcomes.unmade_browser_click_note( "I clicked the upgrade button.", [], prior_success