@miller-tech/uap 1.20.8 → 1.20.10

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.20.8",
3
+ "version": "1.20.10",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,7 +51,7 @@ Configuration (Environment Variables)
51
51
 
52
52
  PROXY_CONTEXT_PRUNE_THRESHOLD Fraction of context window at which
53
53
  conversation pruning activates (0.0-1.0)
54
- Default: 0.75
54
+ Default: 0.85
55
55
 
56
56
  Usage
57
57
  -----
@@ -113,10 +113,10 @@ PROXY_UPSTREAM_RETRY_DELAY_SECS = float(os.environ.get("PROXY_UPSTREAM_RETRY_DEL
113
113
  PROXY_MAX_CONNECTIONS = int(os.environ.get("PROXY_MAX_CONNECTIONS", "20"))
114
114
  PROXY_CONTEXT_WINDOW = int(os.environ.get("PROXY_CONTEXT_WINDOW", "0"))
115
115
  PROXY_CONTEXT_PRUNE_THRESHOLD = float(
116
- os.environ.get("PROXY_CONTEXT_PRUNE_THRESHOLD", "0.75")
116
+ os.environ.get("PROXY_CONTEXT_PRUNE_THRESHOLD", "0.85")
117
117
  )
118
118
  PROXY_CONTEXT_PRUNE_TARGET_FRACTION = float(
119
- os.environ.get("PROXY_CONTEXT_PRUNE_TARGET_FRACTION", "0.65")
119
+ os.environ.get("PROXY_CONTEXT_PRUNE_TARGET_FRACTION", "0.50")
120
120
  )
121
121
  PROXY_LOOP_BREAKER = os.environ.get("PROXY_LOOP_BREAKER", "on").lower() not in {
122
122
  "0",
@@ -277,6 +277,9 @@ PROXY_FORCED_TOOL_DAMPENER_AUTO_TURNS = int(
277
277
  PROXY_FORCED_TOOL_DAMPENER_REJECTIONS = int(
278
278
  os.environ.get("PROXY_FORCED_TOOL_DAMPENER_REJECTIONS", "2")
279
279
  )
280
+ PROXY_TOOL_STARVATION_THRESHOLD = int(
281
+ os.environ.get("PROXY_TOOL_STARVATION_THRESHOLD", "5")
282
+ )
280
283
  PROXY_SESSION_CONTAMINATION_BREAKER = os.environ.get(
281
284
  "PROXY_SESSION_CONTAMINATION_BREAKER", "on"
282
285
  ).lower() not in {
@@ -609,6 +612,7 @@ class SessionMonitor:
609
612
  loop_warnings_emitted: int = 0 # How many loop warnings sent to the model
610
613
  no_progress_streak: int = 0 # Forced tool turns without new tool_result
611
614
  unexpected_end_turn_count: int = 0 # end_turn without tool_use in active loop
615
+ tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
612
616
  malformed_tool_streak: int = 0 # consecutive malformed pseudo tool payloads
613
617
  invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
614
618
  required_tool_miss_streak: int = 0 # required tool turns with no tool call
@@ -2279,6 +2283,29 @@ def build_openai_request(
2279
2283
  last_user_has_tool_result,
2280
2284
  )
2281
2285
 
2286
+ # TOOL STARVATION BREAKER: if model repeatedly fails to produce tool
2287
+ # calls despite required, strip tools to let it generate text and break
2288
+ # the forcing loop.
2289
+ if (
2290
+ monitor.consecutive_forced_count >= PROXY_TOOL_STARVATION_THRESHOLD
2291
+ and _last_assistant_was_text_only(anthropic_body)
2292
+ ):
2293
+ openai_body.pop("tool_choice", None)
2294
+ openai_body.pop("tools", None)
2295
+ monitor.tool_starvation_streak += 1
2296
+ monitor.consecutive_forced_count = 0
2297
+ monitor.no_progress_streak = 0
2298
+ monitor.reset_tool_turn_state(reason="tool_starvation_breaker")
2299
+ logger.warning(
2300
+ "TOOL STARVATION BREAKER: stripped tools after %d forced turns with no tool output (starvation_streak=%d)",
2301
+ PROXY_TOOL_STARVATION_THRESHOLD,
2302
+ monitor.tool_starvation_streak,
2303
+ )
2304
+ # Skip all further tool_choice logic — no tools this turn
2305
+ if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
2306
+ openai_body["enable_thinking"] = False
2307
+ return openai_body
2308
+
2282
2309
  # Check if forced-tool dampener or loop breaker should override tool_choice
2283
2310
  if monitor.consume_forced_auto_turn():
2284
2311
  openai_body["tool_choice"] = "auto"
@@ -2358,6 +2385,7 @@ def build_openai_request(
2358
2385
  if not has_tool_results:
2359
2386
  monitor.reset_tool_turn_state(reason="no_tool_results")
2360
2387
 
2388
+
2361
2389
  if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
2362
2390
  openai_body["enable_thinking"] = False
2363
2391
  logger.info(
@@ -2788,6 +2816,15 @@ _SYSTEM_PROMPT_LEAK_MARKERS = (
2788
2816
  "never emit literal tag artifacts",
2789
2817
  "use tools for concrete work",
2790
2818
  "stopping at analysis",
2819
+ # Client system prompt phrases that also leak into tool args
2820
+ "only produce a final text response without tool calls",
2821
+ "the entire task is fully complete",
2822
+ "always use tools to read, edit, write",
2823
+ "after reading files and identifying an issue",
2824
+ "do not output raw protocol tags",
2825
+ "valid tool call with strict json",
2826
+ "return exactly one valid tool call",
2827
+ "invalid tool call format",
2791
2828
  )
2792
2829
 
2793
2830
 
@@ -3135,6 +3135,37 @@ class TestMinimalSupplementForQwen(unittest.TestCase):
3135
3135
  self.assertIn("agentic-protocol", system_msg)
3136
3136
 
3137
3137
 
3138
+ class TestToolStarvationBreaker(unittest.TestCase):
3139
+ """Tests for tool-call starvation breaker."""
3140
+
3141
+ def _make_body_with_tools(self):
3142
+ return {
3143
+ "model": "qwen3.5",
3144
+ "messages": [
3145
+ {"role": "user", "content": "hello"},
3146
+ {"role": "assistant", "content": "I will help you."},
3147
+ {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "x", "content": "ok"}]},
3148
+ ],
3149
+ "tools": [{"name": "Bash", "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}}}],
3150
+ }
3151
+
3152
+ def test_starvation_breaker_strips_tools(self):
3153
+ monitor = proxy.SessionMonitor()
3154
+ monitor.consecutive_forced_count = proxy.PROXY_TOOL_STARVATION_THRESHOLD
3155
+ body = self._make_body_with_tools()
3156
+ result = proxy.build_openai_request(body, monitor)
3157
+ self.assertNotIn("tools", result)
3158
+ self.assertNotIn("tool_choice", result)
3159
+ self.assertEqual(monitor.tool_starvation_streak, 1)
3160
+
3161
+ def test_no_starvation_below_threshold(self):
3162
+ monitor = proxy.SessionMonitor()
3163
+ monitor.consecutive_forced_count = proxy.PROXY_TOOL_STARVATION_THRESHOLD - 1
3164
+ body = self._make_body_with_tools()
3165
+ result = proxy.build_openai_request(body, monitor)
3166
+ self.assertIn("tools", result)
3167
+
3168
+
3138
3169
  if __name__ == "__main__":
3139
3170
  unittest.main()
3140
3171