@miller-tech/uap 1.64.2 → 1.64.4
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
CHANGED
|
Binary file
|
|
@@ -1005,6 +1005,14 @@ class SessionMonitor:
|
|
|
1005
1005
|
# back into a structured tool_use, which would continue the very loop the
|
|
1006
1006
|
# breaker is ending. Reset to False at the start of every request.
|
|
1007
1007
|
suppress_text_tool_extraction: bool = False
|
|
1008
|
+
# Tool-turn count at which the TURN-COUNT FINALIZE BREAKER last fired. The
|
|
1009
|
+
# count is derived from the (only-growing) conversation, so without this the
|
|
1010
|
+
# breaker would re-fire on EVERY turn once the ceiling is first crossed —
|
|
1011
|
+
# permanently stripping tools and stalling a legitimately long agentic task.
|
|
1012
|
+
# Gating on (last + ceiling) makes it a PERIODIC nudge (80, 160, 240, ...)
|
|
1013
|
+
# with tools restored in between, so long tasks complete while a true runaway
|
|
1014
|
+
# still gets bounded (and the contamination/prune/cycle breakers catch faster).
|
|
1015
|
+
last_hard_finalize_turn_count: int = 0
|
|
1008
1016
|
finalize_continuation_count: int = 0
|
|
1009
1017
|
finalize_hard_stop_count: int = 0 # monotonic, not reset by fresh user text
|
|
1010
1018
|
finalize_synthetic_tool_id: str = ""
|
|
@@ -4095,7 +4103,14 @@ def build_openai_request(
|
|
|
4095
4103
|
# genuine runaway trips it; see PROXY_HARD_FINALIZE_TURNS.
|
|
4096
4104
|
if PROXY_HARD_FINALIZE_TURNS > 0:
|
|
4097
4105
|
_agent_tool_turns = _count_agent_tool_turns(anthropic_body)
|
|
4098
|
-
|
|
4106
|
+
# PERIODIC, not permanent: fire once each time the count crosses
|
|
4107
|
+
# another `ceiling` worth of tool turns past the last firing. Without
|
|
4108
|
+
# the `last + ceiling` gate this fires on EVERY turn past the first
|
|
4109
|
+
# crossing (the count only grows), permanently denying tools and
|
|
4110
|
+
# stalling a long-but-legitimate task. Between fires tools are restored
|
|
4111
|
+
# so the agent keeps making progress.
|
|
4112
|
+
if _agent_tool_turns >= monitor.last_hard_finalize_turn_count + PROXY_HARD_FINALIZE_TURNS:
|
|
4113
|
+
monitor.last_hard_finalize_turn_count = _agent_tool_turns
|
|
4099
4114
|
openai_body.pop("tool_choice", None)
|
|
4100
4115
|
openai_body.pop("tools", None)
|
|
4101
4116
|
openai_body.pop("grammar", None)
|
|
@@ -4103,11 +4118,12 @@ def build_openai_request(
|
|
|
4103
4118
|
msgs.append({
|
|
4104
4119
|
"role": "user",
|
|
4105
4120
|
"content": (
|
|
4106
|
-
f"You have made {_agent_tool_turns} tool calls
|
|
4107
|
-
"
|
|
4108
|
-
"
|
|
4109
|
-
"
|
|
4110
|
-
"
|
|
4121
|
+
f"You have made {_agent_tool_turns} tool calls. Pause for a "
|
|
4122
|
+
"progress checkpoint: in a brief plain-text summary, state "
|
|
4123
|
+
"what is done and the single most important next step. No "
|
|
4124
|
+
"tools are available this turn — do NOT emit any tool call "
|
|
4125
|
+
"or tool-call-like syntax. If the task is complete, say so; "
|
|
4126
|
+
"otherwise you will continue on the next turn."
|
|
4111
4127
|
),
|
|
4112
4128
|
})
|
|
4113
4129
|
openai_body["messages"] = msgs
|
|
@@ -4330,6 +4346,19 @@ def build_openai_request(
|
|
|
4330
4346
|
|
|
4331
4347
|
_apply_thinking_grammar(openai_body)
|
|
4332
4348
|
|
|
4349
|
+
# qwen3.5-enhanced.jinja (the MTP/130 config template) rejects an assistant
|
|
4350
|
+
# PREFILL (trailing assistant message) unless thinking is disabled VIA
|
|
4351
|
+
# chat_template_kwargs — the top-level `enable_thinking` flag is not read by
|
|
4352
|
+
# this template, so a prefill otherwise 400s ("Assistant response prefill is
|
|
4353
|
+
# incompatible with enable_thinking"). There is nothing to think about on a
|
|
4354
|
+
# continuation, so disable thinking the way the template actually reads.
|
|
4355
|
+
_final_msgs = openai_body.get("messages") or []
|
|
4356
|
+
if _final_msgs and isinstance(_final_msgs[-1], dict) and _final_msgs[-1].get("role") == "assistant":
|
|
4357
|
+
ctk = openai_body.setdefault("chat_template_kwargs", {})
|
|
4358
|
+
if isinstance(ctk, dict):
|
|
4359
|
+
ctk["enable_thinking"] = False
|
|
4360
|
+
openai_body.pop("enable_thinking", None)
|
|
4361
|
+
|
|
4333
4362
|
return openai_body
|
|
4334
4363
|
|
|
4335
4364
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The TURN-COUNT FINALIZE BREAKER must fire PERIODICALLY, not on every turn once
|
|
3
|
+
the ceiling is first crossed.
|
|
4
|
+
|
|
5
|
+
`_count_agent_tool_turns` is derived from the (only-growing) conversation, so the
|
|
6
|
+
naive `count >= ceiling` check re-fires on every request past the first crossing —
|
|
7
|
+
permanently stripping tools and stalling a legitimately long agentic task (observed
|
|
8
|
+
live: msgs 206→208→…→214, breaker every turn, then the client gives up). Gating on
|
|
9
|
+
`count >= last_hard_finalize_turn_count + ceiling` makes it fire at ceiling, 2x,
|
|
10
|
+
3x… with tools restored in between.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load():
|
|
19
|
+
p = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", p)
|
|
21
|
+
m = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(m)
|
|
23
|
+
return m
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
proxy = _load()
|
|
27
|
+
CEILING = 80
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fire_turns(counts):
|
|
31
|
+
"""Replay the breaker's gating decision (as in build_openai_request) over a
|
|
32
|
+
sequence of cumulative tool-turn counts; return the counts at which it fired."""
|
|
33
|
+
m = proxy.SessionMonitor(context_window=100000)
|
|
34
|
+
fired = []
|
|
35
|
+
for count in counts:
|
|
36
|
+
if count >= m.last_hard_finalize_turn_count + CEILING:
|
|
37
|
+
m.last_hard_finalize_turn_count = count
|
|
38
|
+
fired.append(count)
|
|
39
|
+
return fired
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestTurnCountBreakerPeriodic(unittest.TestCase):
|
|
43
|
+
def test_monitor_has_the_field_defaulting_zero(self):
|
|
44
|
+
m = proxy.SessionMonitor(context_window=100000)
|
|
45
|
+
self.assertEqual(m.last_hard_finalize_turn_count, 0)
|
|
46
|
+
|
|
47
|
+
def test_fires_periodically_not_every_turn(self):
|
|
48
|
+
# cumulative tool turns 1..244 (grows by 1 each request)
|
|
49
|
+
fired = _fire_turns(range(1, 245))
|
|
50
|
+
# periodic at the ceiling multiples — NOT every turn past 80
|
|
51
|
+
self.assertEqual(fired, [80, 160, 240])
|
|
52
|
+
|
|
53
|
+
def test_does_not_refire_between_crossings(self):
|
|
54
|
+
# turns 80..159 -> fires once at 80, then silent until 160
|
|
55
|
+
fired = _fire_turns(range(80, 160))
|
|
56
|
+
self.assertEqual(fired, [80])
|
|
57
|
+
|
|
58
|
+
def test_first_fire_at_ceiling(self):
|
|
59
|
+
self.assertEqual(_fire_turns(range(1, 81)), [80])
|
|
60
|
+
self.assertEqual(_fire_turns(range(1, 80)), []) # never reaches ceiling
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|