@miller-tech/uap 1.148.6 → 1.148.8

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,69 @@
1
+ #!/usr/bin/env python3
2
+ """Empty-tool-loop breaker: after N consecutive fully-empty responses under a
3
+ forced tool_choice, release the force + inject a plain-text directive so a stuck
4
+ model can report the blocker (e.g. a deleted deliverable) instead of spinning."""
5
+ import importlib.util
6
+ import os
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+
11
+ def _load_proxy():
12
+ os.environ.setdefault("PROXY_EMPTY_TOOL_LOOP_BREAK", "3")
13
+ p = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
14
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", p)
15
+ m = importlib.util.module_from_spec(spec)
16
+ spec.loader.exec_module(m)
17
+ return m
18
+
19
+
20
+ proxy = _load_proxy()
21
+
22
+
23
+ def forced_body():
24
+ return {"tool_choice": "required", "messages": [{"role": "user", "content": "go"}]}
25
+
26
+
27
+ class EmptyToolLoopBreakTests(unittest.TestCase):
28
+ def setUp(self):
29
+ self.m = proxy.SessionMonitor()
30
+
31
+ def test_no_op_below_threshold(self):
32
+ body = forced_body()
33
+ self.m.consecutive_empty_tool_turns = proxy.PROXY_EMPTY_TOOL_LOOP_BREAK - 1
34
+ proxy._maybe_break_empty_tool_loop(body, self.m)
35
+ self.assertEqual(body["tool_choice"], "required") # force intact
36
+ self.assertEqual(len(body["messages"]), 1) # no directive injected
37
+
38
+ def test_releases_and_directs_at_threshold(self):
39
+ body = forced_body()
40
+ self.m.consecutive_empty_tool_turns = proxy.PROXY_EMPTY_TOOL_LOOP_BREAK
41
+ proxy._maybe_break_empty_tool_loop(body, self.m)
42
+ self.assertEqual(body["tool_choice"], "auto") # force released → prose possible
43
+ self.assertEqual(len(body["messages"]), 2) # plain-text directive appended
44
+ self.assertIn("PLAIN TEXT", body["messages"][-1]["content"])
45
+ self.assertEqual(self.m.consecutive_empty_tool_turns, 0) # one-shot per window
46
+
47
+ def test_disabled_when_threshold_zero(self):
48
+ body = forced_body()
49
+ old = proxy.PROXY_EMPTY_TOOL_LOOP_BREAK
50
+ try:
51
+ proxy.PROXY_EMPTY_TOOL_LOOP_BREAK = 0
52
+ self.m.consecutive_empty_tool_turns = 99
53
+ proxy._maybe_break_empty_tool_loop(body, self.m)
54
+ self.assertEqual(body["tool_choice"], "required") # disabled → no-op
55
+ finally:
56
+ proxy.PROXY_EMPTY_TOOL_LOOP_BREAK = old
57
+
58
+ def test_leaves_unforced_tool_choice_alone_but_still_directs(self):
59
+ # If tool_choice isn't 'required' (already auto), don't clobber it, but
60
+ # still surface the directive so the model breaks the empty streak.
61
+ body = {"tool_choice": "auto", "messages": [{"role": "user", "content": "go"}]}
62
+ self.m.consecutive_empty_tool_turns = proxy.PROXY_EMPTY_TOOL_LOOP_BREAK
63
+ proxy._maybe_break_empty_tool_loop(body, self.m)
64
+ self.assertEqual(body["tool_choice"], "auto")
65
+ self.assertEqual(len(body["messages"]), 2)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ unittest.main()