@miller-tech/uap 1.220.4 → 1.220.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """STUCK-BREAK hard tier.
3
+
4
+ The advisory STUCK-BREAK (directive + tool_choice released to 'auto') leaves
5
+ every tool on the table, and a weak local model just issues the same call
6
+ again: observed live 2026-08-23 16:46-17:45, the identical bash call 13+ turns
7
+ in a row with fires=33 and never a prose reply. After
8
+ PROXY_STUCK_BREAK_HARD_FIRES fires the break must be HARD -- tools removed for
9
+ that turn, XML tool-call resurrection suppressed -- so the turn can only end in
10
+ prose and the client's agent loop actually terminates.
11
+ """
12
+
13
+ import importlib.util
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+
18
+ def _load_proxy_module():
19
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
20
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
21
+ assert spec is not None and spec.loader is not None
22
+ module = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ proxy = _load_proxy_module()
28
+
29
+
30
+ def _looping_monitor(fires_so_far: int):
31
+ mon = proxy.SessionMonitor()
32
+ mon.self_stuck_streak = proxy.PROXY_STUCK_TEXT_THRESHOLD + 1
33
+ mon.stuck_break_fires = fires_so_far
34
+ return mon
35
+
36
+
37
+ def _body():
38
+ return {
39
+ "tool_choice": "required",
40
+ "tools": [{"type": "function", "function": {"name": "bash", "parameters": {}}}],
41
+ "messages": [{"role": "system", "content": "sys"}, {"role": "user", "content": "go"}],
42
+ }
43
+
44
+
45
+ class TestStuckBreakHard(unittest.TestCase):
46
+ def test_below_hard_threshold_stays_advisory(self):
47
+ mon = _looping_monitor(fires_so_far=0)
48
+ body = _body()
49
+ proxy._maybe_inject_stuck_break(body, mon)
50
+ self.assertEqual(mon.stuck_break_fires, 1)
51
+ self.assertIn("tools", body, "advisory tier keeps the tools")
52
+ self.assertEqual(body["tool_choice"], "auto")
53
+ self.assertFalse(mon.suppress_text_tool_extraction)
54
+ self.assertIn("STOP", body["messages"][0]["content"])
55
+
56
+ def test_hard_threshold_strips_tools_and_suppresses_xml_resurrection(self):
57
+ mon = _looping_monitor(fires_so_far=proxy.PROXY_STUCK_BREAK_HARD_FIRES - 1)
58
+ body = _body()
59
+ proxy._maybe_inject_stuck_break(body, mon)
60
+ self.assertEqual(mon.stuck_break_fires, proxy.PROXY_STUCK_BREAK_HARD_FIRES)
61
+ self.assertNotIn("tools", body)
62
+ self.assertNotIn("tool_choice", body)
63
+ self.assertTrue(mon.suppress_text_tool_extraction)
64
+ self.assertIn("plain text", body["messages"][0]["content"])
65
+ self.assertIn("STOP", body["messages"][0]["content"]) # the advisory text still travels
66
+
67
+ def test_hard_tier_can_be_disabled(self):
68
+ saved = proxy.PROXY_STUCK_BREAK_HARD_FIRES
69
+ try:
70
+ proxy.PROXY_STUCK_BREAK_HARD_FIRES = 0
71
+ mon = _looping_monitor(fires_so_far=50)
72
+ body = _body()
73
+ proxy._maybe_inject_stuck_break(body, mon)
74
+ self.assertIn("tools", body)
75
+ self.assertFalse(mon.suppress_text_tool_extraction)
76
+ finally:
77
+ proxy.PROXY_STUCK_BREAK_HARD_FIRES = saved
78
+
79
+ def test_default_threshold_is_small(self):
80
+ # Three ignored breaks is already ~8 minutes of identical calls on the
81
+ # local executor; the default must not let a loop run for an hour.
82
+ self.assertGreater(proxy.PROXY_STUCK_BREAK_HARD_FIRES, 0)
83
+ self.assertLessEqual(proxy.PROXY_STUCK_BREAK_HARD_FIRES, 5)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ unittest.main()