@miller-tech/uap 1.118.1 → 1.118.2
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.
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +93 -11
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +20 -11
- package/tools/agents/tests/test_cycle_break_exploration.py +149 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -340,6 +340,83 @@ _WRITE_TOOL_CLASS = frozenset({
|
|
|
340
340
|
"write_file", "edit_file", "save_file",
|
|
341
341
|
})
|
|
342
342
|
|
|
343
|
+
# Open-ended exploration tools the agent uses to make a DIFFERENT move once a
|
|
344
|
+
# read/grep/glob loop is broken. The code cannot enumerate every exploration
|
|
345
|
+
# tool, but these are the universal escape hatches. Cycle-break narrowing must
|
|
346
|
+
# NEVER strip these: a cycling *Bash* means "vary the command" (handled by the
|
|
347
|
+
# injected cycle hint), NOT "lose the ability to run bash at all". Excluding
|
|
348
|
+
# Bash here stranded autonomous ("auto mode") agents with no way to explore the
|
|
349
|
+
# filesystem — the whole toolset minus its exploration escape hatch.
|
|
350
|
+
_EXPLORATION_ESCAPE_TOOLS = frozenset({
|
|
351
|
+
"bash", "shell", "sh", "run_bash", "runbash", "run_command",
|
|
352
|
+
"run_terminal_cmd", "execute_command", "executecommand", "terminal",
|
|
353
|
+
"webfetch", "web_fetch", "fetch", "websearch", "web_search",
|
|
354
|
+
"agent", "task", "dispatch_agent",
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _narrow_tools_for_cycle_break(tools, cycling_tool_names, session_banned_tools):
|
|
359
|
+
"""Drop cycling + session-banned tools from the toolset, expanding to the
|
|
360
|
+
whole read-only class when any excluded tool is read-only.
|
|
361
|
+
|
|
362
|
+
The open-ended exploration escape hatch (Bash/WebFetch/Agent) is kept out of
|
|
363
|
+
the CYCLING-derived exclusion: a cycling Bash means "vary the command"
|
|
364
|
+
(handled by the injected cycle hint), not "lose the ability to run bash".
|
|
365
|
+
An explicit session ban is a stronger, deliberate signal (a tool that keeps
|
|
366
|
+
malforming) and IS still honored, even for those tools.
|
|
367
|
+
|
|
368
|
+
Returns ``(narrowed_tools, expanded_read_only_class)``. Tool-name matching is
|
|
369
|
+
case-insensitive so "Bash"/"bash" and "Read"/"read" behave identically.
|
|
370
|
+
"""
|
|
371
|
+
read_only_lower = {c.lower() for c in _READ_ONLY_TOOL_CLASS}
|
|
372
|
+
cycling_lower = {n.lower() for n in cycling_tool_names}
|
|
373
|
+
banned_lower = {n.lower() for n in session_banned_tools}
|
|
374
|
+
# Expand to the full read-only class if any excluded tool is read-only
|
|
375
|
+
# (preserves the original trigger over cycling ∪ banned).
|
|
376
|
+
expanded = any(n in read_only_lower for n in (cycling_lower | banned_lower))
|
|
377
|
+
cycling_exclude = set(cycling_lower)
|
|
378
|
+
if expanded:
|
|
379
|
+
cycling_exclude |= read_only_lower
|
|
380
|
+
# Never let the cycling path narrow away the exploration escape hatch — that
|
|
381
|
+
# is exactly the filesystem-exploration capability the cycle-break is trying
|
|
382
|
+
# to redirect the agent toward.
|
|
383
|
+
cycling_exclude -= _EXPLORATION_ESCAPE_TOOLS
|
|
384
|
+
exclude_set = cycling_exclude | banned_lower
|
|
385
|
+
|
|
386
|
+
def _name(t):
|
|
387
|
+
return ((t.get("function", {}) or {}).get("name", "") or "").lower()
|
|
388
|
+
|
|
389
|
+
narrowed = [t for t in tools if _name(t) not in exclude_set]
|
|
390
|
+
|
|
391
|
+
# Floor invariant: a loop-breaker must never strand the agent. If narrowing
|
|
392
|
+
# would remove the last way to make a DIFFERENT move — i.e. no exploration
|
|
393
|
+
# escape hatch AND no write tool survives, but the original set had one —
|
|
394
|
+
# keep the original toolset. This closes the door where an explicit ban of
|
|
395
|
+
# the sole exploration tool re-creates the very "can't explore" bug.
|
|
396
|
+
write_lower = {w.lower() for w in _WRITE_TOOL_CLASS}
|
|
397
|
+
|
|
398
|
+
def _has_action_path(tool_list):
|
|
399
|
+
names = {_name(t) for t in tool_list}
|
|
400
|
+
return bool(names & _EXPLORATION_ESCAPE_TOOLS) or bool(names & write_lower)
|
|
401
|
+
|
|
402
|
+
if _has_action_path(tools) and not _has_action_path(narrowed):
|
|
403
|
+
return list(tools), expanded
|
|
404
|
+
return narrowed, expanded
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _should_auto_ban(name, cycle_count, ban_at):
|
|
408
|
+
"""Whether a cycling tool should be added to session_banned_tools this round.
|
|
409
|
+
|
|
410
|
+
The auto-ban accumulator is itself cycling-derived (ban after N detections),
|
|
411
|
+
so exploration escape-hatch tools (Bash/WebFetch/Agent) are NEVER auto-banned
|
|
412
|
+
— banning Bash would re-strip the agent's only way to explore the filesystem
|
|
413
|
+
on the Nth cycle, re-creating the bug the narrowing exemption fixes for
|
|
414
|
+
earlier cycles. Repeated Bash is redirected via the injected cycle hint.
|
|
415
|
+
"""
|
|
416
|
+
if name.lower() in _EXPLORATION_ESCAPE_TOOLS:
|
|
417
|
+
return False
|
|
418
|
+
return cycle_count >= ban_at
|
|
419
|
+
|
|
343
420
|
PROXY_GUARDRAIL_RETRY = os.environ.get("PROXY_GUARDRAIL_RETRY", "on").lower() not in {
|
|
344
421
|
"0",
|
|
345
422
|
"false",
|
|
@@ -4028,7 +4105,13 @@ def _resolve_state_machine_tool_choice(
|
|
|
4028
4105
|
and PROXY_COORDINATION_BAN_THRESHOLD > 0
|
|
4029
4106
|
)
|
|
4030
4107
|
ban_at = PROXY_COORDINATION_BAN_THRESHOLD if is_coord else 3
|
|
4031
|
-
|
|
4108
|
+
# Exploration escape-hatch tools are never auto-banned — see
|
|
4109
|
+
# _should_auto_ban (banning Bash re-creates the "can't explore"
|
|
4110
|
+
# bug since the ban is itself cycling-derived).
|
|
4111
|
+
if (
|
|
4112
|
+
_should_auto_ban(name, monitor.tool_cycle_counts[name], ban_at)
|
|
4113
|
+
and name not in monitor.session_banned_tools
|
|
4114
|
+
):
|
|
4032
4115
|
monitor.session_banned_tools.add(name)
|
|
4033
4116
|
logger.warning(
|
|
4034
4117
|
"TOOL BAN: '%s' banned for session after %d cycle detections "
|
|
@@ -4853,16 +4936,15 @@ def build_openai_request(
|
|
|
4853
4936
|
(monitor.cycling_tool_names or monitor.session_banned_tools)
|
|
4854
4937
|
and "tools" in openai_body
|
|
4855
4938
|
):
|
|
4856
|
-
exclude_set = set(monitor.cycling_tool_names) | monitor.session_banned_tools
|
|
4857
|
-
# Expand to full read-only class if any cycling tool is read-only
|
|
4858
|
-
if any(n.lower() in {c.lower() for c in _READ_ONLY_TOOL_CLASS} for n in exclude_set):
|
|
4859
|
-
exclude_set |= _READ_ONLY_TOOL_CLASS
|
|
4860
4939
|
original_count = len(openai_body["tools"])
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4940
|
+
# Drop cycling/banned tools (and the read-only class if any
|
|
4941
|
+
# cycling tool is read-only) but always keep the exploration
|
|
4942
|
+
# escape hatch — see _narrow_tools_for_cycle_break.
|
|
4943
|
+
narrowed, expanded_read_only = _narrow_tools_for_cycle_break(
|
|
4944
|
+
openai_body["tools"],
|
|
4945
|
+
monitor.cycling_tool_names,
|
|
4946
|
+
monitor.session_banned_tools,
|
|
4947
|
+
)
|
|
4866
4948
|
if narrowed:
|
|
4867
4949
|
openai_body["tools"] = narrowed
|
|
4868
4950
|
# Only log on first activation or phase transitions to reduce noise
|
|
@@ -4872,7 +4954,7 @@ def build_openai_request(
|
|
|
4872
4954
|
original_count,
|
|
4873
4955
|
len(narrowed),
|
|
4874
4956
|
monitor.cycling_tool_names,
|
|
4875
|
-
|
|
4957
|
+
expanded_read_only,
|
|
4876
4958
|
)
|
|
4877
4959
|
else:
|
|
4878
4960
|
logger.warning(
|
|
@@ -3465,8 +3465,10 @@ class TestCycleBreakOptions(unittest.TestCase):
|
|
|
3465
3465
|
setattr(proxy, "PROXY_TOOL_STATE_STAGNATION_THRESHOLD", old_stagnation)
|
|
3466
3466
|
setattr(proxy, "PROXY_TOOL_STATE_CYCLE_WINDOW", old_cycle_window)
|
|
3467
3467
|
|
|
3468
|
-
def
|
|
3469
|
-
"""
|
|
3468
|
+
def test_cycle_break_keeps_bash_exploration(self):
|
|
3469
|
+
"""Cycling on Bash must NOT strip the Bash tool — it is the agent's
|
|
3470
|
+
exploration escape hatch. (Regression: the cycle-break used to exclude
|
|
3471
|
+
Bash by name, blocking all filesystem exploration.)"""
|
|
3470
3472
|
old_state = getattr(proxy, "PROXY_TOOL_STATE_MACHINE")
|
|
3471
3473
|
old_min_msgs = getattr(proxy, "PROXY_TOOL_STATE_MIN_MESSAGES")
|
|
3472
3474
|
old_forced = getattr(proxy, "PROXY_TOOL_STATE_FORCED_BUDGET")
|
|
@@ -3516,9 +3518,10 @@ class TestCycleBreakOptions(unittest.TestCase):
|
|
|
3516
3518
|
|
|
3517
3519
|
openai = proxy.build_openai_request(body, monitor)
|
|
3518
3520
|
self.assertEqual(monitor.tool_turn_phase, "review")
|
|
3519
|
-
# Bash
|
|
3521
|
+
# Bash is the exploration escape hatch — it must remain available
|
|
3522
|
+
# even while cycling. Read and Write remain too.
|
|
3520
3523
|
tool_names = [t["function"]["name"] for t in openai.get("tools", [])]
|
|
3521
|
-
self.
|
|
3524
|
+
self.assertIn("Bash", tool_names)
|
|
3522
3525
|
self.assertIn("Read", tool_names)
|
|
3523
3526
|
self.assertIn("Write", tool_names)
|
|
3524
3527
|
finally:
|
|
@@ -4548,7 +4551,8 @@ class TestReadOnlyCycleClassExclusion(unittest.TestCase):
|
|
|
4548
4551
|
setattr(proxy, k, v)
|
|
4549
4552
|
|
|
4550
4553
|
def test_non_read_tool_cycling_no_class_expansion(self):
|
|
4551
|
-
"""When 'bash' is cycling, only
|
|
4554
|
+
"""When 'bash' is cycling, the read-only class is NOT expanded, and bash
|
|
4555
|
+
itself is preserved as the exploration escape hatch."""
|
|
4552
4556
|
old_vals = {
|
|
4553
4557
|
"PROXY_TOOL_STATE_MACHINE": getattr(proxy, "PROXY_TOOL_STATE_MACHINE"),
|
|
4554
4558
|
"PROXY_TOOL_STATE_MIN_MESSAGES": getattr(proxy, "PROXY_TOOL_STATE_MIN_MESSAGES"),
|
|
@@ -4581,8 +4585,9 @@ class TestReadOnlyCycleClassExclusion(unittest.TestCase):
|
|
|
4581
4585
|
remaining_names = [
|
|
4582
4586
|
t.get("function", {}).get("name") for t in openai_body.get("tools", [])
|
|
4583
4587
|
]
|
|
4584
|
-
|
|
4585
|
-
|
|
4588
|
+
# bash is an exploration escape hatch — cycling does not strip it.
|
|
4589
|
+
self.assertIn("bash", remaining_names)
|
|
4590
|
+
# Read-only tools should still be available (no class expansion)
|
|
4586
4591
|
self.assertIn("read", remaining_names)
|
|
4587
4592
|
self.assertIn("glob", remaining_names)
|
|
4588
4593
|
self.assertIn("grep", remaining_names)
|
|
@@ -4685,16 +4690,20 @@ class TestPersistentCycleExclusion(unittest.TestCase):
|
|
|
4685
4690
|
body = self._make_body_with_tools(all_tools)
|
|
4686
4691
|
monitor = proxy.SessionMonitor(context_window=262144)
|
|
4687
4692
|
|
|
4688
|
-
# Simulate
|
|
4689
|
-
|
|
4693
|
+
# Simulate a non-exploration tool cycling that triggers review. (Bash
|
|
4694
|
+
# is exempt from cycling exclusion, so use 'edit' to exercise the
|
|
4695
|
+
# persistence path.)
|
|
4696
|
+
monitor.cycling_tool_names = ["edit"]
|
|
4690
4697
|
monitor.tool_turn_phase = "act"
|
|
4691
4698
|
monitor.tool_state_forced_budget_remaining = 5
|
|
4692
4699
|
|
|
4693
4700
|
openai = proxy.build_openai_request(body, monitor)
|
|
4694
4701
|
|
|
4695
|
-
# In act phase with cycling_tool_names set,
|
|
4702
|
+
# In act phase with cycling_tool_names set, the cycling tool stays
|
|
4703
|
+
# excluded — but the exploration escape hatch (bash) is preserved.
|
|
4696
4704
|
remaining = [t["function"]["name"] for t in openai.get("tools", [])]
|
|
4697
|
-
self.assertNotIn("
|
|
4705
|
+
self.assertNotIn("edit", remaining)
|
|
4706
|
+
self.assertIn("bash", remaining)
|
|
4698
4707
|
self.assertIn("read", remaining)
|
|
4699
4708
|
self.assertIn("write", remaining)
|
|
4700
4709
|
finally:
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for cycle-break tool narrowing preserving the exploration escape hatch.
|
|
3
|
+
|
|
4
|
+
Regression: the CYCLE BREAK narrowing removed every cycling tool by name (and the
|
|
5
|
+
whole read-only class if any cycling tool was read-only). Because Bash can be a
|
|
6
|
+
cycling tool, the agent lost the Bash tool entirely and could no longer run any
|
|
7
|
+
bash command to explore the filesystem. The narrowing must keep the open-ended
|
|
8
|
+
exploration escape hatch (Bash/WebFetch/Agent) available so the agent can make a
|
|
9
|
+
DIFFERENT move — the cycle *hint* handles "vary the command".
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import unittest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_proxy_module():
|
|
18
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
20
|
+
assert spec is not None and spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
proxy = _load_proxy_module()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _tools(*names):
|
|
30
|
+
return [{"function": {"name": n}} for n in names]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _names(tools):
|
|
34
|
+
return sorted(t["function"]["name"] for t in tools)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestCycleBreakExploration(unittest.TestCase):
|
|
38
|
+
def test_cycling_bash_keeps_bash(self):
|
|
39
|
+
"""The core bug: cycling on Bash must NOT remove the Bash tool."""
|
|
40
|
+
tools = _tools("Bash", "Read", "Write")
|
|
41
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(tools, {"Bash"}, set())
|
|
42
|
+
self.assertIn("Bash", _names(narrowed))
|
|
43
|
+
self.assertFalse(expanded) # Bash is not read-only, so no class expansion
|
|
44
|
+
|
|
45
|
+
def test_read_only_cycle_expands_class_but_keeps_exploration(self):
|
|
46
|
+
"""Cycling a read-only tool drops the whole read-only class, yet the
|
|
47
|
+
exploration escape hatch (Bash/WebFetch/Agent) survives."""
|
|
48
|
+
tools = _tools("Read", "Grep", "Glob", "Bash", "WebFetch", "Agent", "Write")
|
|
49
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(tools, {"Grep"}, set())
|
|
50
|
+
self.assertTrue(expanded)
|
|
51
|
+
kept = _names(narrowed)
|
|
52
|
+
# read-only class dropped
|
|
53
|
+
for n in ("Read", "Grep", "Glob"):
|
|
54
|
+
self.assertNotIn(n, kept)
|
|
55
|
+
# exploration escape hatch + writes preserved
|
|
56
|
+
for n in ("Bash", "WebFetch", "Agent", "Write"):
|
|
57
|
+
self.assertIn(n, kept)
|
|
58
|
+
|
|
59
|
+
def test_session_ban_is_honored_even_for_exploration_tools(self):
|
|
60
|
+
"""An explicit session ban is a stronger, deliberate signal than cycling
|
|
61
|
+
and IS honored — even for an exploration tool. (Only the cycling path
|
|
62
|
+
preserves the escape hatch.)"""
|
|
63
|
+
tools = _tools("Bash", "Read", "Write")
|
|
64
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, set(), {"Bash"})
|
|
65
|
+
self.assertNotIn("Bash", _names(narrowed))
|
|
66
|
+
|
|
67
|
+
def test_cycling_bash_kept_even_when_another_tool_is_banned(self):
|
|
68
|
+
"""A cycling Bash is preserved even while an unrelated tool is banned."""
|
|
69
|
+
tools = _tools("Bash", "Read", "Task")
|
|
70
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, {"Bash"}, {"Task"})
|
|
71
|
+
kept = _names(narrowed)
|
|
72
|
+
self.assertIn("Bash", kept) # cycling exploration hatch preserved
|
|
73
|
+
self.assertNotIn("Task", kept) # explicit ban honored
|
|
74
|
+
|
|
75
|
+
def test_case_insensitive_matching(self):
|
|
76
|
+
"""Lowercase 'bash' in the cycling set must still match a 'Bash' tool
|
|
77
|
+
(and keep it), and 'read' must drop a 'Read' tool."""
|
|
78
|
+
tools = _tools("Bash", "Read")
|
|
79
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(
|
|
80
|
+
tools, {"bash", "read"}, set()
|
|
81
|
+
)
|
|
82
|
+
kept = _names(narrowed)
|
|
83
|
+
self.assertIn("Bash", kept) # exploration hatch kept despite cycling
|
|
84
|
+
self.assertNotIn("Read", kept) # read-only dropped
|
|
85
|
+
self.assertTrue(expanded)
|
|
86
|
+
|
|
87
|
+
def test_floor_keeps_last_action_tool_when_ban_would_strip_it(self):
|
|
88
|
+
"""Floor invariant: if honoring a ban would leave NO exploration tool and
|
|
89
|
+
NO write tool, keep the original set — a loop-breaker must never strand
|
|
90
|
+
the agent with no way to make a different move."""
|
|
91
|
+
tools = _tools("Bash", "Read") # Read is read-only, not an action path
|
|
92
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, set(), {"Bash"})
|
|
93
|
+
self.assertIn("Bash", _names(narrowed)) # floor restored the escape hatch
|
|
94
|
+
|
|
95
|
+
def test_floor_not_triggered_when_a_write_tool_survives(self):
|
|
96
|
+
"""The ban IS honored when another action path (a write tool) remains."""
|
|
97
|
+
tools = _tools("Bash", "Read", "Write")
|
|
98
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, set(), {"Bash"})
|
|
99
|
+
kept = _names(narrowed)
|
|
100
|
+
self.assertNotIn("Bash", kept) # honored: Write still gives an action path
|
|
101
|
+
self.assertIn("Write", kept)
|
|
102
|
+
|
|
103
|
+
def test_tool_class_taxonomy_is_pairwise_disjoint(self):
|
|
104
|
+
"""Regression insurance: the three tool-class sets must never overlap, or
|
|
105
|
+
convergence accounting / cycle-break scope would silently change."""
|
|
106
|
+
ro = {c.lower() for c in proxy._READ_ONLY_TOOL_CLASS}
|
|
107
|
+
wr = {c.lower() for c in proxy._WRITE_TOOL_CLASS}
|
|
108
|
+
ex = {c.lower() for c in proxy._EXPLORATION_ESCAPE_TOOLS}
|
|
109
|
+
self.assertEqual(ro & wr, set())
|
|
110
|
+
self.assertEqual(ro & ex, set())
|
|
111
|
+
self.assertEqual(wr & ex, set())
|
|
112
|
+
|
|
113
|
+
def test_non_exploration_non_readonly_tool_dropped(self):
|
|
114
|
+
"""A cycling tool that is neither read-only nor an exploration hatch is
|
|
115
|
+
dropped, and the read-only class is NOT expanded."""
|
|
116
|
+
tools = _tools("SomeTool", "Read", "Bash")
|
|
117
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(
|
|
118
|
+
tools, {"SomeTool"}, set()
|
|
119
|
+
)
|
|
120
|
+
kept = _names(narrowed)
|
|
121
|
+
self.assertNotIn("SomeTool", kept)
|
|
122
|
+
self.assertIn("Read", kept) # class not expanded, Read survives
|
|
123
|
+
self.assertIn("Bash", kept)
|
|
124
|
+
self.assertFalse(expanded)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestAutoBanExemption(unittest.TestCase):
|
|
128
|
+
"""The session-ban accumulator is cycling-derived (ban after N detections).
|
|
129
|
+
Exploration escape-hatch tools must never be auto-banned, or the 'cannot
|
|
130
|
+
explore the filesystem' bug reappears on the Nth cycle."""
|
|
131
|
+
|
|
132
|
+
def test_bash_never_auto_banned_even_far_past_threshold(self):
|
|
133
|
+
self.assertFalse(proxy._should_auto_ban("bash", cycle_count=99, ban_at=3))
|
|
134
|
+
self.assertFalse(proxy._should_auto_ban("Bash", cycle_count=3, ban_at=3))
|
|
135
|
+
|
|
136
|
+
def test_other_exploration_tools_never_auto_banned(self):
|
|
137
|
+
for name in ("webfetch", "WebSearch", "agent", "task"):
|
|
138
|
+
self.assertFalse(proxy._should_auto_ban(name, cycle_count=10, ban_at=3))
|
|
139
|
+
|
|
140
|
+
def test_non_exploration_tool_banned_at_threshold(self):
|
|
141
|
+
self.assertTrue(proxy._should_auto_ban("edit", cycle_count=3, ban_at=3))
|
|
142
|
+
self.assertTrue(proxy._should_auto_ban("read", cycle_count=3, ban_at=3))
|
|
143
|
+
|
|
144
|
+
def test_non_exploration_tool_not_banned_below_threshold(self):
|
|
145
|
+
self.assertFalse(proxy._should_auto_ban("edit", cycle_count=2, ban_at=3))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
unittest.main()
|