@miller-tech/uap 1.172.0 → 1.172.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.
- package/config/llama-profiles/gemma4-26b-a4b-mtp.env +145 -0
- package/config/llama-profiles/qwen36-35b-a3b.env +69 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/worktree.d.ts +11 -0
- package/dist/cli/worktree.d.ts.map +1 -1
- package/dist/cli/worktree.js +30 -1
- package/dist/cli/worktree.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts +7 -0
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +42 -1
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/interaction/runner.d.ts +10 -0
- package/dist/delivery/interaction/runner.d.ts.map +1 -1
- package/dist/delivery/interaction/runner.js +93 -11
- package/dist/delivery/interaction/runner.js.map +1 -1
- package/dist/delivery/interaction-gate.d.ts.map +1 -1
- package/dist/delivery/interaction-gate.js +7 -0
- package/dist/delivery/interaction-gate.js.map +1 -1
- package/dist/delivery/visual-gate.js.map +1 -1
- package/dist/models/openai-compat-client.d.ts.map +1 -1
- package/dist/models/openai-compat-client.js +68 -1
- package/dist/models/openai-compat-client.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/enforcement_self_protect.py +96 -5
- package/templates/hooks/__pycache__/deliver_autoroute.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 +94 -1
- package/tools/agents/tests/test_mandate_beats_recon.py +188 -0
- package/tools/agents/tests/test_mandate_deliver.py +35 -2
|
@@ -1468,6 +1468,7 @@ class SessionMonitor:
|
|
|
1468
1468
|
doubling_break_fires: int = 0 # monotonic count of injected pivot directives
|
|
1469
1469
|
last_doubling_obs: str = "" # msg-count:fingerprint key of the last counted observation
|
|
1470
1470
|
mandate_deliver_fires: int = 0 # monotonic count of forced deliver-routings (mandate)
|
|
1471
|
+
mandate_deliver_active: bool = False # THIS turn is pinned to deliver (beats recon convergence)
|
|
1471
1472
|
tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
|
|
1472
1473
|
last_request_msg_count: int = 0 # Message count of the previous request (compaction-boundary detection)
|
|
1473
1474
|
malformed_tool_streak: int = 0 # consecutive malformed pseudo tool payloads
|
|
@@ -5251,6 +5252,13 @@ def _maybe_inject_deferral_break(openai_body: dict, monitor: "SessionMonitor") -
|
|
|
5251
5252
|
PROXY_MANDATE_DELIVER = os.environ.get("PROXY_MANDATE_DELIVER", "on").lower() not in {
|
|
5252
5253
|
"0", "off", "false", "no",
|
|
5253
5254
|
}
|
|
5255
|
+
# Precedence: when the mandate pins a turn to deliver, recon convergence must not
|
|
5256
|
+
# undo it. Both guards run on the same turn and both rewrite tools/tool_choice, so
|
|
5257
|
+
# without this the later one (recon) silently wins. Off => the old ordering, where
|
|
5258
|
+
# recon can release the pin mid-mandate.
|
|
5259
|
+
PROXY_MANDATE_BEATS_RECON = os.environ.get(
|
|
5260
|
+
"PROXY_MANDATE_BEATS_RECON", "on"
|
|
5261
|
+
).lower() not in {"0", "off", "false", "no"}
|
|
5254
5262
|
# Enforcer-block-SPECIFIC phrases only (src/policies/enforcers/delivery_enforcement.py).
|
|
5255
5263
|
# Deliberately NOT matching the reactor's STANDING "route through deliver" guidance
|
|
5256
5264
|
# (which is injected every turn) -- only the actual block event must trigger.
|
|
@@ -5318,11 +5326,65 @@ def _assistant_already_called_deliver(messages, deliver_name: str) -> bool:
|
|
|
5318
5326
|
return False
|
|
5319
5327
|
|
|
5320
5328
|
|
|
5329
|
+
def _pin_tool_choice_to(openai_body: dict, tool_name: str) -> bool:
|
|
5330
|
+
"""Constrain this turn to calling exactly `tool_name`. Returns True if pinned.
|
|
5331
|
+
|
|
5332
|
+
The obvious encoding -- OpenAI's `{"type": "function", "function": {"name":
|
|
5333
|
+
...}}` -- does NOT work against llama.cpp. Its server reads the field as a
|
|
5334
|
+
STRING (`json_value(body, "tool_choice", std::string("auto"))`) and accepts
|
|
5335
|
+
only auto | none | required via common_chat_tool_choice_parse_oaicompat().
|
|
5336
|
+
Handed an object it logs
|
|
5337
|
+
|
|
5338
|
+
Wrong type supplied for parameter 'tool_choice'. Expected 'string',
|
|
5339
|
+
using default value
|
|
5340
|
+
|
|
5341
|
+
and falls back to "auto" -- so every "pin" silently became "the model may do
|
|
5342
|
+
whatever it likes". That is how a blocked source edit could be answered with
|
|
5343
|
+
"I will proceed by manually creating the files": the mandate was never a
|
|
5344
|
+
constraint, only the directive text asking nicely.
|
|
5345
|
+
|
|
5346
|
+
Express the same intent in the vocabulary llama.cpp actually has: demand SOME
|
|
5347
|
+
tool call, and narrow the advertised tools to just this one, so the only call
|
|
5348
|
+
that satisfies the demand is the intended one. "required" is equally valid
|
|
5349
|
+
OpenAI, so this stays correct against a non-llama.cpp upstream.
|
|
5350
|
+
|
|
5351
|
+
Narrowing is skipped if the tool is not present in `tools` -- sending
|
|
5352
|
+
"required" with the tool absent would force a call the model cannot make.
|
|
5353
|
+
"""
|
|
5354
|
+
tools = openai_body.get("tools")
|
|
5355
|
+
if not isinstance(tools, list):
|
|
5356
|
+
return False
|
|
5357
|
+
wanted = str(tool_name).lower()
|
|
5358
|
+
pinned = [
|
|
5359
|
+
t for t in tools
|
|
5360
|
+
if isinstance(t, dict)
|
|
5361
|
+
and str((t.get("function") or {}).get("name") or "").lower() == wanted
|
|
5362
|
+
]
|
|
5363
|
+
if not pinned:
|
|
5364
|
+
return False
|
|
5365
|
+
openai_body["tools"] = pinned
|
|
5366
|
+
openai_body["tool_choice"] = "required"
|
|
5367
|
+
return True
|
|
5368
|
+
|
|
5369
|
+
|
|
5321
5370
|
def _maybe_inject_mandate_deliver(openai_body: dict, monitor: "SessionMonitor") -> None:
|
|
5322
5371
|
"""MANDATORY deliver-routing: when a direct source edit was just blocked by
|
|
5323
5372
|
delivery-enforcement, force the next turn to call the `deliver` tool for ANY
|
|
5324
5373
|
model. Pins tool_choice to the deliver tool + injects a terse directive.
|
|
5325
5374
|
Runs before the softer guards so the pin stands. PROXY_MANDATE_DELIVER=off."""
|
|
5375
|
+
# Decide the per-turn flag HERE, unconditionally, before any early return.
|
|
5376
|
+
#
|
|
5377
|
+
# It previously lived beside finalize_turn_active in build_openai_request --
|
|
5378
|
+
# which sits inside `if has_tools:`, while this function and
|
|
5379
|
+
# _maybe_inject_recon_convergence are both called unconditionally. On a
|
|
5380
|
+
# TOOL-LESS turn the reset was therefore skipped, this function returned
|
|
5381
|
+
# early (no deliver tool to find), and recon read a stale True from an
|
|
5382
|
+
# earlier turn and suppressed itself. Owning the flag here makes it
|
|
5383
|
+
# impossible to leave stale: the one function that can set it True is the
|
|
5384
|
+
# same one that always clears it first, on every turn, whatever the caller
|
|
5385
|
+
# does. The clear precedes the PROXY_MANDATE_DELIVER check deliberately, so
|
|
5386
|
+
# disabling the mandate can never strand a True either.
|
|
5387
|
+
monitor.mandate_deliver_active = False
|
|
5326
5388
|
if not PROXY_MANDATE_DELIVER:
|
|
5327
5389
|
return
|
|
5328
5390
|
deliver_name = _deliver_tool_name(openai_body.get("tools"))
|
|
@@ -5333,7 +5395,13 @@ def _maybe_inject_mandate_deliver(openai_body: dict, monitor: "SessionMonitor")
|
|
|
5333
5395
|
return
|
|
5334
5396
|
if _assistant_already_called_deliver(messages, deliver_name):
|
|
5335
5397
|
return # deliver is already in flight -- don't loop on it
|
|
5336
|
-
|
|
5398
|
+
if not _pin_tool_choice_to(openai_body, deliver_name):
|
|
5399
|
+
# Could not actually constrain the turn (deliver absent from `tools`).
|
|
5400
|
+
# Don't claim a mandate that isn't real: recording one here would
|
|
5401
|
+
# suppress recon convergence while leaving the model free to hand-edit,
|
|
5402
|
+
# which is strictly worse than letting recon do its job.
|
|
5403
|
+
return
|
|
5404
|
+
monitor.mandate_deliver_active = True
|
|
5337
5405
|
monitor.mandate_deliver_fires += 1
|
|
5338
5406
|
directive = (
|
|
5339
5407
|
"\n\nMANDATORY: your direct source edit was BLOCKED by delivery-enforcement. "
|
|
@@ -5419,6 +5487,31 @@ def _maybe_inject_recon_convergence(
|
|
|
5419
5487
|
"""
|
|
5420
5488
|
if PROXY_RECON_CONVERGENCE_THRESHOLD <= 0:
|
|
5421
5489
|
return
|
|
5490
|
+
# PRECEDENCE: the deliver mandate outranks recon convergence.
|
|
5491
|
+
#
|
|
5492
|
+
# Both run on the same turn (mandate first), and both rewrite tools/
|
|
5493
|
+
# tool_choice, so the later one wins by default -- which made the mandate's
|
|
5494
|
+
# own "runs before the softer guards so the pin stands" untrue. Until
|
|
5495
|
+
# v1.172.2 that was invisible: the pin was encoded in a form llama.cpp
|
|
5496
|
+
# ignored, so it had nothing to override.
|
|
5497
|
+
#
|
|
5498
|
+
# The mandate is the more specific and more recent signal: a source edit was
|
|
5499
|
+
# just BLOCKED and the only correct next action is a deliver call. Recon
|
|
5500
|
+
# exists to break a read-forever deadlock -- which cannot occur while the
|
|
5501
|
+
# mandate holds, because the pin leaves exactly one advertised tool under
|
|
5502
|
+
# tool_choice="required", so the model is constrained to call deliver rather
|
|
5503
|
+
# than read again. Letting recon win instead released that constraint and
|
|
5504
|
+
# handed back the freedom to keep hand-editing.
|
|
5505
|
+
#
|
|
5506
|
+
# Escape hatch if this ever wedges: PROXY_MANDATE_BEATS_RECON=off.
|
|
5507
|
+
if PROXY_MANDATE_BEATS_RECON and getattr(monitor, "mandate_deliver_active", False):
|
|
5508
|
+
logger.info(
|
|
5509
|
+
"RECON CONVERGENCE: suppressed -- deliver mandate owns this turn "
|
|
5510
|
+
"(no_write_streak=%d, mandate_fires=%d).",
|
|
5511
|
+
monitor.consecutive_no_write_turns,
|
|
5512
|
+
monitor.mandate_deliver_fires,
|
|
5513
|
+
)
|
|
5514
|
+
return
|
|
5422
5515
|
streak = monitor.consecutive_no_write_turns
|
|
5423
5516
|
if streak < PROXY_RECON_CONVERGENCE_THRESHOLD:
|
|
5424
5517
|
return
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Precedence: the deliver mandate outranks recon convergence.
|
|
3
|
+
|
|
4
|
+
Both guards run on the same turn (mandate first) and both rewrite
|
|
5
|
+
tools/tool_choice, so without an explicit rule the later one -- recon -- wins
|
|
6
|
+
and silently releases the pin. That made the mandate's own docstring claim
|
|
7
|
+
("runs before the softer guards so the pin stands") untrue.
|
|
8
|
+
|
|
9
|
+
The regression is only observable now: until v1.172.2 the pin was encoded in a
|
|
10
|
+
form llama.cpp ignored, so recon had nothing to override.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_proxy(env: dict | None = None):
|
|
20
|
+
"""Import the proxy with `env` applied, then restore the environment.
|
|
21
|
+
|
|
22
|
+
Module-level constants (PROXY_*) are read at import, so a knob can only be
|
|
23
|
+
exercised by re-importing under a different environment.
|
|
24
|
+
"""
|
|
25
|
+
saved = {k: os.environ.get(k) for k in (env or {})}
|
|
26
|
+
try:
|
|
27
|
+
for k, v in (env or {}).items():
|
|
28
|
+
os.environ[k] = v
|
|
29
|
+
p = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
30
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy_mbr", p)
|
|
31
|
+
assert spec is not None and spec.loader is not None
|
|
32
|
+
m = importlib.util.module_from_spec(spec)
|
|
33
|
+
spec.loader.exec_module(m)
|
|
34
|
+
return m
|
|
35
|
+
finally:
|
|
36
|
+
for k, v in saved.items():
|
|
37
|
+
if v is None:
|
|
38
|
+
os.environ.pop(k, None)
|
|
39
|
+
else:
|
|
40
|
+
os.environ[k] = v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
proxy = _load_proxy()
|
|
44
|
+
|
|
45
|
+
DELIVER_TOOL = {"type": "function", "function": {"name": "deliver", "parameters": {}}}
|
|
46
|
+
READ_TOOL = {"type": "function", "function": {"name": "read", "parameters": {}}}
|
|
47
|
+
GREP_TOOL = {"type": "function", "function": {"name": "grep", "parameters": {}}}
|
|
48
|
+
BLOCK_MSG = (
|
|
49
|
+
"BLOCKED: do not edit 'src/foo.ts' directly. To create or change code, call "
|
|
50
|
+
"the `deliver` tool (or run: uap deliver \"...\"). Do NOT retry this edit."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _blocked_body(tools):
|
|
55
|
+
return {
|
|
56
|
+
"tools": list(tools),
|
|
57
|
+
"messages": [
|
|
58
|
+
{"role": "assistant", "content": "editing",
|
|
59
|
+
"tool_calls": [{"function": {"name": "edit"}}]},
|
|
60
|
+
{"role": "tool", "content": BLOCK_MSG},
|
|
61
|
+
],
|
|
62
|
+
"tool_choice": "auto",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _deep_recon(monitor, mod):
|
|
67
|
+
"""Put the monitor well past the hard recon tier, so recon would definitely
|
|
68
|
+
fire and rewrite the request if it were allowed to run."""
|
|
69
|
+
monitor.consecutive_no_write_turns = int(
|
|
70
|
+
mod.PROXY_RECON_CONVERGENCE_THRESHOLD * mod.PROXY_RECON_HARD_MULTIPLIER
|
|
71
|
+
) + 10
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MandateBeatsReconTest(unittest.TestCase):
|
|
75
|
+
def test_recon_cannot_release_a_pinned_turn(self):
|
|
76
|
+
mon = proxy.SessionMonitor()
|
|
77
|
+
mon.mandate_deliver_active = False
|
|
78
|
+
body = _blocked_body([READ_TOOL, GREP_TOOL, DELIVER_TOOL])
|
|
79
|
+
|
|
80
|
+
proxy._maybe_inject_mandate_deliver(body, mon)
|
|
81
|
+
self.assertTrue(mon.mandate_deliver_active)
|
|
82
|
+
self.assertEqual(body["tool_choice"], "required")
|
|
83
|
+
self.assertEqual([t["function"]["name"] for t in body["tools"]], ["deliver"])
|
|
84
|
+
|
|
85
|
+
_deep_recon(mon, proxy)
|
|
86
|
+
proxy._maybe_inject_recon_convergence(body, mon, [READ_TOOL, GREP_TOOL, DELIVER_TOOL])
|
|
87
|
+
|
|
88
|
+
# The pin must survive: still required, still exactly one tool.
|
|
89
|
+
self.assertEqual(body["tool_choice"], "required")
|
|
90
|
+
self.assertEqual([t["function"]["name"] for t in body["tools"]], ["deliver"])
|
|
91
|
+
|
|
92
|
+
def test_recon_still_runs_when_no_mandate_is_active(self):
|
|
93
|
+
"""The suppression must be scoped to mandated turns -- recon is the
|
|
94
|
+
read-forever deadlock breaker and must keep working otherwise."""
|
|
95
|
+
mon = proxy.SessionMonitor()
|
|
96
|
+
mon.mandate_deliver_active = False
|
|
97
|
+
body = {"tools": [READ_TOOL, GREP_TOOL], "messages": [{"role": "user", "content": "explore"}]}
|
|
98
|
+
_deep_recon(mon, proxy)
|
|
99
|
+
|
|
100
|
+
before = mon.recon_hard_fires
|
|
101
|
+
proxy._maybe_inject_recon_convergence(body, mon, [READ_TOOL, GREP_TOOL])
|
|
102
|
+
self.assertGreater(mon.recon_hard_fires, before)
|
|
103
|
+
|
|
104
|
+
def test_flag_is_cleared_on_a_TOOL_LESS_turn(self):
|
|
105
|
+
"""Regression: the reset used to live beside finalize_turn_active in
|
|
106
|
+
build_openai_request, which sits inside `if has_tools:` -- while both
|
|
107
|
+
_maybe_inject_mandate_deliver and _maybe_inject_recon_convergence are
|
|
108
|
+
called unconditionally. A tool-less turn therefore skipped the reset,
|
|
109
|
+
the mandate returned early (no deliver tool to find), and recon read a
|
|
110
|
+
stale True from an earlier turn and suppressed itself.
|
|
111
|
+
|
|
112
|
+
The mandate now owns the flag and clears it on every turn, so this
|
|
113
|
+
sequence -- mandate turn, then a turn with no tools at all -- must leave
|
|
114
|
+
it False.
|
|
115
|
+
"""
|
|
116
|
+
mon = proxy.SessionMonitor()
|
|
117
|
+
proxy._maybe_inject_mandate_deliver(_blocked_body([DELIVER_TOOL]), mon)
|
|
118
|
+
self.assertTrue(mon.mandate_deliver_active)
|
|
119
|
+
|
|
120
|
+
proxy._maybe_inject_mandate_deliver({"messages": [{"role": "user", "content": "hi"}]}, mon)
|
|
121
|
+
self.assertFalse(mon.mandate_deliver_active)
|
|
122
|
+
|
|
123
|
+
def test_flag_is_cleared_even_when_the_mandate_is_disabled(self):
|
|
124
|
+
"""The clear precedes the PROXY_MANDATE_DELIVER check, so turning the
|
|
125
|
+
mandate off cannot strand a True set while it was on."""
|
|
126
|
+
mod = _load_proxy({"PROXY_MANDATE_DELIVER": "off"})
|
|
127
|
+
self.assertFalse(mod.PROXY_MANDATE_DELIVER)
|
|
128
|
+
mon = mod.SessionMonitor()
|
|
129
|
+
mon.mandate_deliver_active = True # as if set on a previous turn
|
|
130
|
+
mod._maybe_inject_mandate_deliver(_blocked_body([DELIVER_TOOL]), mon)
|
|
131
|
+
self.assertFalse(mon.mandate_deliver_active)
|
|
132
|
+
|
|
133
|
+
def test_flag_is_per_turn_not_sticky(self):
|
|
134
|
+
"""A stale True would suppress recon forever after one mandate. The
|
|
135
|
+
request builder clears it each turn; assert the field is not latched by
|
|
136
|
+
the mandate itself."""
|
|
137
|
+
mon = proxy.SessionMonitor()
|
|
138
|
+
self.assertFalse(mon.mandate_deliver_active)
|
|
139
|
+
body = _blocked_body([DELIVER_TOOL])
|
|
140
|
+
proxy._maybe_inject_mandate_deliver(body, mon)
|
|
141
|
+
self.assertTrue(mon.mandate_deliver_active)
|
|
142
|
+
|
|
143
|
+
# Simulate the next turn's reset, then a turn with no block present.
|
|
144
|
+
mon.mandate_deliver_active = False
|
|
145
|
+
clean = {"tools": [DELIVER_TOOL], "messages": [{"role": "user", "content": "hello"}]}
|
|
146
|
+
proxy._maybe_inject_mandate_deliver(clean, mon)
|
|
147
|
+
self.assertFalse(mon.mandate_deliver_active)
|
|
148
|
+
|
|
149
|
+
def test_no_mandate_claimed_when_pin_is_impossible(self):
|
|
150
|
+
"""If deliver is absent the pin cannot be applied. Recording a mandate
|
|
151
|
+
anyway would suppress recon while leaving the model free to hand-edit --
|
|
152
|
+
strictly worse than letting recon run."""
|
|
153
|
+
mon = proxy.SessionMonitor()
|
|
154
|
+
body = _blocked_body([READ_TOOL])
|
|
155
|
+
proxy._maybe_inject_mandate_deliver(body, mon)
|
|
156
|
+
self.assertFalse(mon.mandate_deliver_active)
|
|
157
|
+
self.assertEqual(mon.mandate_deliver_fires, 0)
|
|
158
|
+
|
|
159
|
+
def test_precedence_off_reproduces_the_bug_it_fixes(self):
|
|
160
|
+
"""PROXY_MANDATE_BEATS_RECON=off restores the old ordering -- and pins
|
|
161
|
+
down exactly what that ordering costs.
|
|
162
|
+
|
|
163
|
+
This is the load-bearing assertion of the whole change: with the
|
|
164
|
+
precedence disabled, recon's hard tier sets tool_choice back to "auto",
|
|
165
|
+
releasing a mandate raised because a source edit had just been BLOCKED.
|
|
166
|
+
The model is then free to do what it was blocked for. If this test ever
|
|
167
|
+
stops reproducing that release, the precedence guard has become dead
|
|
168
|
+
code and the suppression above is no longer protecting anything.
|
|
169
|
+
"""
|
|
170
|
+
mod = _load_proxy({"PROXY_MANDATE_BEATS_RECON": "off"})
|
|
171
|
+
self.assertFalse(mod.PROXY_MANDATE_BEATS_RECON)
|
|
172
|
+
|
|
173
|
+
mon = mod.SessionMonitor()
|
|
174
|
+
body = _blocked_body([READ_TOOL, GREP_TOOL, DELIVER_TOOL])
|
|
175
|
+
mod._maybe_inject_mandate_deliver(body, mon)
|
|
176
|
+
self.assertTrue(mon.mandate_deliver_active)
|
|
177
|
+
self.assertEqual(body["tool_choice"], "required")
|
|
178
|
+
|
|
179
|
+
_deep_recon(mon, mod)
|
|
180
|
+
before = mon.recon_hard_fires
|
|
181
|
+
mod._maybe_inject_recon_convergence(body, mon, [READ_TOOL, GREP_TOOL, DELIVER_TOOL])
|
|
182
|
+
|
|
183
|
+
self.assertGreater(mon.recon_hard_fires, before) # recon ran
|
|
184
|
+
self.assertEqual(body["tool_choice"], "auto") # ...and released the pin
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
unittest.main()
|
|
@@ -46,7 +46,11 @@ class MandateDeliverTest(unittest.TestCase):
|
|
|
46
46
|
{"role": "tool", "content": BLOCK_MSG},
|
|
47
47
|
], tools=[DELIVER_TOOL])
|
|
48
48
|
proxy._maybe_inject_mandate_deliver(b, self.mon)
|
|
49
|
-
|
|
49
|
+
# "required" + a one-tool list, NOT the OpenAI object form: llama.cpp
|
|
50
|
+
# parses tool_choice as a string and silently falls back to "auto" when
|
|
51
|
+
# handed an object, which un-pinned every mandate.
|
|
52
|
+
self.assertEqual(b["tool_choice"], "required")
|
|
53
|
+
self.assertEqual([t["function"]["name"] for t in b["tools"]], ["deliver"])
|
|
50
54
|
self.assertEqual(self.mon.mandate_deliver_fires, 1)
|
|
51
55
|
self.assertTrue(any("MANDATORY" in (m.get("content") or "") for m in b["messages"]))
|
|
52
56
|
|
|
@@ -79,7 +83,36 @@ class MandateDeliverTest(unittest.TestCase):
|
|
|
79
83
|
mcp_deliver = {"type": "function", "function": {"name": "mcp__uap-router__deliver", "parameters": {}}}
|
|
80
84
|
b = _body([{"role": "tool", "content": BLOCK_MSG}], tools=[mcp_deliver])
|
|
81
85
|
proxy._maybe_inject_mandate_deliver(b, self.mon)
|
|
82
|
-
self.assertEqual(b["tool_choice"],
|
|
86
|
+
self.assertEqual(b["tool_choice"], "required")
|
|
87
|
+
self.assertEqual(
|
|
88
|
+
[t["function"]["name"] for t in b["tools"]], ["mcp__uap-router__deliver"]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def test_pin_narrows_tools_to_the_single_mandated_tool(self):
|
|
92
|
+
"""The pin must REMOVE the other tools. 'required' only means "some
|
|
93
|
+
tool", so leaving `read` advertised would let the model satisfy the
|
|
94
|
+
mandate by reading a file instead of delivering."""
|
|
95
|
+
b = _body([{"role": "tool", "content": BLOCK_MSG}], tools=[READ_TOOL, DELIVER_TOOL])
|
|
96
|
+
proxy._maybe_inject_mandate_deliver(b, self.mon)
|
|
97
|
+
self.assertEqual(b["tool_choice"], "required")
|
|
98
|
+
self.assertEqual([t["function"]["name"] for t in b["tools"]], ["deliver"])
|
|
99
|
+
|
|
100
|
+
def test_tool_choice_is_never_an_object(self):
|
|
101
|
+
"""Regression guard for the llama.cpp string-only parser. An object here
|
|
102
|
+
is not a hard error upstream -- it degrades to "auto" with only a log
|
|
103
|
+
line -- so nothing downstream would have caught the regression."""
|
|
104
|
+
b = _body([{"role": "tool", "content": BLOCK_MSG}], tools=[DELIVER_TOOL])
|
|
105
|
+
proxy._maybe_inject_mandate_deliver(b, self.mon)
|
|
106
|
+
self.assertIsInstance(b["tool_choice"], str)
|
|
107
|
+
self.assertIn(b["tool_choice"], {"auto", "none", "required"})
|
|
108
|
+
|
|
109
|
+
def test_pin_is_a_noop_when_the_named_tool_is_absent(self):
|
|
110
|
+
"""Forcing "required" while the mandated tool is missing would demand a
|
|
111
|
+
call the model cannot make. _pin_tool_choice_to must decline instead."""
|
|
112
|
+
b = _body([{"role": "user", "content": "hi"}], tools=[READ_TOOL], tool_choice="auto")
|
|
113
|
+
self.assertFalse(proxy._pin_tool_choice_to(b, "deliver"))
|
|
114
|
+
self.assertEqual(b["tool_choice"], "auto")
|
|
115
|
+
self.assertEqual([t["function"]["name"] for t in b["tools"]], ["read"])
|
|
83
116
|
|
|
84
117
|
def test_disabled_by_env(self):
|
|
85
118
|
old = os.environ.get("PROXY_MANDATE_DELIVER")
|