@miller-tech/uap 1.101.0 → 1.101.1
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
|
|
Binary file
|
|
@@ -205,6 +205,32 @@ PROXY_FORCED_HARD_RELEASE = int(os.environ.get("PROXY_FORCED_HARD_RELEASE", "30"
|
|
|
205
205
|
PROXY_CONTEXT_RELEASE_THRESHOLD = float(
|
|
206
206
|
os.environ.get("PROXY_CONTEXT_RELEASE_THRESHOLD", "0.90")
|
|
207
207
|
)
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# STUCK-BREAK guardrail: a small model can recognize it is looping ("I've been
|
|
210
|
+
# stuck in a loop, let me break out") yet keep repeating the SAME failing tool
|
|
211
|
+
# call -- meta-cognition without an exit. Two signals, both observed live on a
|
|
212
|
+
# qwen3.6 session that looped ~18min fetching a rate-limited GitHub API:
|
|
213
|
+
# (a) repeated self-reported "stuck" assistant text, and
|
|
214
|
+
# (b) repeated tool calls hitting a known rate-limited API host.
|
|
215
|
+
# When either streak crosses its threshold the proxy forces a TERMINAL turn:
|
|
216
|
+
# tool_choice back to auto + a firm directive to stop retrying and either
|
|
217
|
+
# proceed without the unreachable resource or ask the operator. Default on;
|
|
218
|
+
# PROXY_STUCK_BREAK=off to disable.
|
|
219
|
+
PROXY_STUCK_BREAK = os.environ.get("PROXY_STUCK_BREAK", "on").lower() not in {
|
|
220
|
+
"0", "false", "off", "no",
|
|
221
|
+
}
|
|
222
|
+
# Self-reported-stuck phrases (lowercased match). Deliberately narrow.
|
|
223
|
+
_STUCK_PHRASE_RE = re.compile(
|
|
224
|
+
r"stuck in a loop|been stuck|break out of (?:this|the) loop|going in circles|"
|
|
225
|
+
r"repeating myself|same (?:thing|error) (?:again|repeatedly)",
|
|
226
|
+
re.IGNORECASE,
|
|
227
|
+
)
|
|
228
|
+
# Tool args reaching into a rate-limited REST API host (the wrong channel; the
|
|
229
|
+
# hint steers to the browser tool / git clone, which are not rate-limited).
|
|
230
|
+
_RATE_LIMITED_API_RE = re.compile(r"api\.github\.com", re.IGNORECASE)
|
|
231
|
+
PROXY_STUCK_TEXT_THRESHOLD = int(os.environ.get("PROXY_STUCK_TEXT_THRESHOLD", "2"))
|
|
232
|
+
PROXY_STUCK_API_THRESHOLD = int(os.environ.get("PROXY_STUCK_API_THRESHOLD", "3"))
|
|
233
|
+
|
|
208
234
|
PROXY_TOOL_STATE_MACHINE = os.environ.get(
|
|
209
235
|
"PROXY_TOOL_STATE_MACHINE", "on"
|
|
210
236
|
).lower() not in {
|
|
@@ -1128,6 +1154,9 @@ class SessionMonitor:
|
|
|
1128
1154
|
recon_hard_fires: int = 0 # Fix E: monotonic count of recon hard-tier firings
|
|
1129
1155
|
catastrophic_ctx_streak: int = 0 # Fix F: consecutive turns raw ctx >= finalize ratio
|
|
1130
1156
|
unexpected_end_turn_count: int = 0 # end_turn without tool_use in active loop
|
|
1157
|
+
self_stuck_streak: int = 0 # consecutive assistant texts self-reporting a loop
|
|
1158
|
+
rate_limited_api_streak: int = 0 # consecutive tool calls hitting a rate-limited API host
|
|
1159
|
+
stuck_break_fires: int = 0 # monotonic count of forced stuck-breaks
|
|
1131
1160
|
tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
|
|
1132
1161
|
malformed_tool_streak: int = 0 # consecutive malformed pseudo tool payloads
|
|
1133
1162
|
invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
|
|
@@ -1363,6 +1392,40 @@ class SessionMonitor:
|
|
|
1363
1392
|
by_tool = self.tool_target_history.setdefault(name, {})
|
|
1364
1393
|
by_tool[target] = by_tool.get(target, 0) + 1
|
|
1365
1394
|
|
|
1395
|
+
def note_assistant_text(self, text: str) -> None:
|
|
1396
|
+
"""Track the model self-reporting that it is stuck (STUCK-BREAK signal
|
|
1397
|
+
(a)). A matching turn increments the streak; a non-matching turn resets
|
|
1398
|
+
it, so only SUSTAINED self-reported looping trips the break."""
|
|
1399
|
+
if not PROXY_STUCK_BREAK or not text:
|
|
1400
|
+
self.self_stuck_streak = 0
|
|
1401
|
+
return
|
|
1402
|
+
if _STUCK_PHRASE_RE.search(text):
|
|
1403
|
+
self.self_stuck_streak += 1
|
|
1404
|
+
else:
|
|
1405
|
+
self.self_stuck_streak = 0
|
|
1406
|
+
|
|
1407
|
+
def note_tool_arg_hosts(self, arg_blobs: list) -> None:
|
|
1408
|
+
"""Track repeated tool calls into a rate-limited API host (STUCK-BREAK
|
|
1409
|
+
signal (b)). Reset when a turn uses none, so only a sustained wrong-
|
|
1410
|
+
channel loop trips the hint."""
|
|
1411
|
+
if not PROXY_STUCK_BREAK:
|
|
1412
|
+
return
|
|
1413
|
+
blob = " ".join(a for a in arg_blobs if isinstance(a, str))
|
|
1414
|
+
if _RATE_LIMITED_API_RE.search(blob):
|
|
1415
|
+
self.rate_limited_api_streak += 1
|
|
1416
|
+
else:
|
|
1417
|
+
self.rate_limited_api_streak = 0
|
|
1418
|
+
|
|
1419
|
+
def should_force_stuck_break(self) -> tuple[bool, str]:
|
|
1420
|
+
"""True + reason when a terminal break should be forced this turn."""
|
|
1421
|
+
if not PROXY_STUCK_BREAK:
|
|
1422
|
+
return False, ""
|
|
1423
|
+
if self.self_stuck_streak >= PROXY_STUCK_TEXT_THRESHOLD:
|
|
1424
|
+
return True, f"self-reported stuck x{self.self_stuck_streak}"
|
|
1425
|
+
if self.rate_limited_api_streak >= PROXY_STUCK_API_THRESHOLD:
|
|
1426
|
+
return True, f"rate-limited-API retries x{self.rate_limited_api_streak}"
|
|
1427
|
+
return False, ""
|
|
1428
|
+
|
|
1366
1429
|
def has_duplicate_read_target(self, threshold: int = 2) -> tuple[bool, str]:
|
|
1367
1430
|
"""Check if any read-only tool has re-read the same target >= threshold times.
|
|
1368
1431
|
|
|
@@ -4067,6 +4130,36 @@ def _writes_are_gated(openai_body: dict) -> bool:
|
|
|
4067
4130
|
return False
|
|
4068
4131
|
|
|
4069
4132
|
|
|
4133
|
+
def _maybe_inject_stuck_break(openai_body: dict, monitor: "SessionMonitor") -> None:
|
|
4134
|
+
"""Force a terminal turn when the model is looping self-awarely or hammering
|
|
4135
|
+
a rate-limited API. Unlike the cycle-breaker (which narrows tools), this
|
|
4136
|
+
STOPS tool coercion and tells the model to synthesize / ask / route around
|
|
4137
|
+
the unreachable resource -- converting the model's own "I'm stuck" into an
|
|
4138
|
+
actual exit. Fires at most escalating; monotonic counter for telemetry."""
|
|
4139
|
+
should, reason = monitor.should_force_stuck_break()
|
|
4140
|
+
if not should:
|
|
4141
|
+
return
|
|
4142
|
+
monitor.stuck_break_fires += 1
|
|
4143
|
+
# Release the tool-choice coercion so a plain text turn is allowed.
|
|
4144
|
+
if openai_body.get("tool_choice") == "required":
|
|
4145
|
+
openai_body["tool_choice"] = "auto"
|
|
4146
|
+
directive = (
|
|
4147
|
+
"\n\nSTOP — you are repeating a failing action (" + reason + "). Do NOT "
|
|
4148
|
+
"retry the same tool or fetch again. If a resource is unreachable (e.g. a "
|
|
4149
|
+
"rate-limited GitHub REST API), switch channel: use the browser tool or "
|
|
4150
|
+
"`git clone` (git protocol), NOT api.github.com. If it is still "
|
|
4151
|
+
"unavailable, proceed WITHOUT it using what you already have, or ask the "
|
|
4152
|
+
"operator the single blocking question in one sentence. Take a DIFFERENT "
|
|
4153
|
+
"action now."
|
|
4154
|
+
)
|
|
4155
|
+
msgs = openai_body.get("messages") or []
|
|
4156
|
+
if msgs and msgs[0].get("role") == "system":
|
|
4157
|
+
msgs[0]["content"] = (msgs[0].get("content") or "") + directive
|
|
4158
|
+
else:
|
|
4159
|
+
msgs.insert(0, {"role": "system", "content": directive.strip()})
|
|
4160
|
+
logger.warning("STUCK-BREAK: forced terminal turn (%s, fires=%d)", reason, monitor.stuck_break_fires)
|
|
4161
|
+
|
|
4162
|
+
|
|
4070
4163
|
def _maybe_inject_recon_convergence(
|
|
4071
4164
|
openai_body: dict,
|
|
4072
4165
|
monitor: "SessionMonitor",
|
|
@@ -4846,6 +4939,8 @@ def build_openai_request(
|
|
|
4846
4939
|
# pre-narrowing toolset so it can restore a dropped write tool.
|
|
4847
4940
|
_maybe_inject_recon_convergence(openai_body, monitor, full_openai_tools)
|
|
4848
4941
|
|
|
4942
|
+
_maybe_inject_stuck_break(openai_body, monitor)
|
|
4943
|
+
|
|
4849
4944
|
_apply_thinking_grammar(openai_body)
|
|
4850
4945
|
|
|
4851
4946
|
_apply_json_response_grammar(openai_body, anthropic_body)
|
|
@@ -8612,6 +8707,12 @@ async def stream_anthropic_response(
|
|
|
8612
8707
|
tc_names,
|
|
8613
8708
|
[a[:200] for a in tc_args],
|
|
8614
8709
|
)
|
|
8710
|
+
# STUCK-BREAK signals: feed the assistant text + tool args to the monitor.
|
|
8711
|
+
try:
|
|
8712
|
+
monitor.note_assistant_text(accumulated_text)
|
|
8713
|
+
monitor.note_tool_arg_hosts(list(tc_args))
|
|
8714
|
+
except Exception:
|
|
8715
|
+
pass
|
|
8615
8716
|
|
|
8616
8717
|
# -------------------------------------------------------------------
|
|
8617
8718
|
# Post-stream: recover <tool_call> XML from accumulated text
|