@miller-tech/uap 1.15.9 → 1.15.11

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.15.9",
3
+ "version": "1.15.11",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1134,6 +1134,28 @@ def _has_tool_definitions(anthropic_body: dict) -> bool:
1134
1134
  return isinstance(tools, list) and len(tools) > 0
1135
1135
 
1136
1136
 
1137
+ def _should_use_guarded_non_stream(
1138
+ is_stream: bool,
1139
+ anthropic_body: dict,
1140
+ openai_body: dict,
1141
+ ) -> bool:
1142
+ if not is_stream:
1143
+ return False
1144
+
1145
+ if PROXY_FORCE_NON_STREAM:
1146
+ return True
1147
+
1148
+ has_tools = _has_tool_definitions(anthropic_body)
1149
+ if PROXY_MALFORMED_TOOL_STREAM_STRICT and has_tools:
1150
+ return True
1151
+
1152
+ return (
1153
+ has_tools
1154
+ and openai_body.get("tool_choice") == "required"
1155
+ and (PROXY_MALFORMED_TOOL_GUARDRAIL or PROXY_GUARDRAIL_RETRY)
1156
+ )
1157
+
1158
+
1137
1159
  def _message_has_tool_result(content) -> bool:
1138
1160
  return isinstance(content, list) and any(
1139
1161
  isinstance(block, dict) and block.get("type") == "tool_result"
@@ -2479,16 +2501,14 @@ def _classify_tool_response_issue(
2479
2501
  has_tool_calls = _openai_has_tool_calls(openai_resp)
2480
2502
  if not has_tool_calls:
2481
2503
  if required_tool_choice:
2482
- text = _openai_message_text(openai_resp).strip()
2483
- if not text or len(text) <= 48:
2484
- return ToolResponseIssue(
2485
- kind="required_tool_miss",
2486
- reason="required tool turn returned no tool calls",
2487
- retry_hint=(
2488
- "A tool call is mandatory for this turn. Emit exactly one valid tool call now "
2489
- "with a strict JSON object in `arguments`."
2490
- ),
2491
- )
2504
+ return ToolResponseIssue(
2505
+ kind="required_tool_miss",
2506
+ reason="required tool turn returned no tool calls",
2507
+ retry_hint=(
2508
+ "A tool call is mandatory for this turn. Emit exactly one valid tool call now "
2509
+ "with a strict JSON object in `arguments`."
2510
+ ),
2511
+ )
2492
2512
  return ToolResponseIssue()
2493
2513
 
2494
2514
  if not PROXY_TOOL_ARGS_PREFLIGHT:
@@ -2566,6 +2586,49 @@ def _looks_malformed_tool_payload(text: str) -> bool:
2566
2586
  return True
2567
2587
  if lowered.count("</parameter") >= 1 and lowered.count('{"description"') >= 1:
2568
2588
  return True
2589
+ if _looks_repetitive_policy_echo(text):
2590
+ return True
2591
+ return False
2592
+
2593
+
2594
+ def _looks_repetitive_policy_echo(text: str) -> bool:
2595
+ if not text:
2596
+ return False
2597
+
2598
+ lowered = text.lower()
2599
+ compact = re.sub(r"\s+", " ", lowered).strip()
2600
+ if not compact:
2601
+ return False
2602
+
2603
+ policy_phrase_markers = (
2604
+ "at least 2 new test cases",
2605
+ "tests must be in test/",
2606
+ "describe/it/expect using vitest",
2607
+ )
2608
+ if any(compact.count(marker) >= 4 for marker in policy_phrase_markers):
2609
+ return True
2610
+
2611
+ lines = [
2612
+ re.sub(r"\s+", " ", line.strip().lower())
2613
+ for line in text.splitlines()
2614
+ if line.strip()
2615
+ ]
2616
+ if lines:
2617
+ line_counts: dict[str, int] = {}
2618
+ for line in lines:
2619
+ if len(line) < 24:
2620
+ continue
2621
+ line_counts[line] = line_counts.get(line, 0) + 1
2622
+ if line_counts and max(line_counts.values()) >= 8:
2623
+ return True
2624
+
2625
+ repeated_phrase_match = re.search(
2626
+ r"((?:[a-z0-9_./-]+\s+){2,8}[a-z0-9_./-]+)(?:\s+\1){7,}",
2627
+ compact,
2628
+ )
2629
+ if repeated_phrase_match:
2630
+ return True
2631
+
2569
2632
  return False
2570
2633
 
2571
2634
 
@@ -3509,9 +3572,10 @@ async def messages(request: Request):
3509
3572
  media_type="application/json",
3510
3573
  )
3511
3574
 
3512
- use_guarded_non_stream = is_stream and (
3513
- PROXY_FORCE_NON_STREAM
3514
- or (PROXY_MALFORMED_TOOL_STREAM_STRICT and "tools" in body)
3575
+ use_guarded_non_stream = _should_use_guarded_non_stream(
3576
+ is_stream,
3577
+ body,
3578
+ openai_body,
3515
3579
  )
3516
3580
  if use_guarded_non_stream:
3517
3581
  strict_body = dict(openai_body)
@@ -3582,10 +3646,14 @@ async def messages(request: Request):
3582
3646
  logger.info(
3583
3647
  "FORCED NON-STREAM: served stream response via guarded non-stream path"
3584
3648
  )
3585
- else:
3649
+ elif PROXY_MALFORMED_TOOL_STREAM_STRICT and _has_tool_definitions(body):
3586
3650
  logger.info(
3587
3651
  "STRICT STREAM GUARDRAIL: served stream response via guarded non-stream path"
3588
3652
  )
3653
+ else:
3654
+ logger.info(
3655
+ "REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
3656
+ )
3589
3657
 
3590
3658
  return StreamingResponse(
3591
3659
  stream_anthropic_message(anthropic_resp),
@@ -100,6 +100,54 @@ class TestProxyConfigTuning(unittest.TestCase):
100
100
  setattr(proxy, "PROXY_CONTEXT_PRUNE_TARGET_FRACTION", old_target)
101
101
 
102
102
 
103
+ class TestStreamGuardedPathSelection(unittest.TestCase):
104
+ def test_required_tool_turn_uses_guarded_non_stream(self):
105
+ old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")
106
+ old_strict = getattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT")
107
+ old_guard = getattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL")
108
+ old_retry = getattr(proxy, "PROXY_GUARDRAIL_RETRY")
109
+ try:
110
+ setattr(proxy, "PROXY_FORCE_NON_STREAM", False)
111
+ setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", False)
112
+ setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", True)
113
+ setattr(proxy, "PROXY_GUARDRAIL_RETRY", True)
114
+
115
+ selected = proxy._should_use_guarded_non_stream(
116
+ True,
117
+ {"tools": [{"name": "Read", "input_schema": {"type": "object"}}]},
118
+ {"tool_choice": "required"},
119
+ )
120
+ self.assertTrue(selected)
121
+ finally:
122
+ setattr(proxy, "PROXY_FORCE_NON_STREAM", old_force)
123
+ setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", old_strict)
124
+ setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", old_guard)
125
+ setattr(proxy, "PROXY_GUARDRAIL_RETRY", old_retry)
126
+
127
+ def test_auto_tool_turn_keeps_true_stream_when_strict_off(self):
128
+ old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")
129
+ old_strict = getattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT")
130
+ old_guard = getattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL")
131
+ old_retry = getattr(proxy, "PROXY_GUARDRAIL_RETRY")
132
+ try:
133
+ setattr(proxy, "PROXY_FORCE_NON_STREAM", False)
134
+ setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", False)
135
+ setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", True)
136
+ setattr(proxy, "PROXY_GUARDRAIL_RETRY", True)
137
+
138
+ selected = proxy._should_use_guarded_non_stream(
139
+ True,
140
+ {"tools": [{"name": "Read", "input_schema": {"type": "object"}}]},
141
+ {"tool_choice": "auto"},
142
+ )
143
+ self.assertFalse(selected)
144
+ finally:
145
+ setattr(proxy, "PROXY_FORCE_NON_STREAM", old_force)
146
+ setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", old_strict)
147
+ setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", old_guard)
148
+ setattr(proxy, "PROXY_GUARDRAIL_RETRY", old_retry)
149
+
150
+
103
151
  class TestMalformedToolGuardrail(unittest.TestCase):
104
152
  def test_detects_malformed_tool_payload(self):
105
153
  openai_resp = {
@@ -805,6 +853,54 @@ class TestMalformedToolGuardrail(unittest.TestCase):
805
853
  )
806
854
  self.assertEqual(issue.kind, "required_tool_miss")
807
855
 
856
+ def test_required_tool_turn_with_long_text_without_tool_call_is_flagged(self):
857
+ openai_resp = {
858
+ "choices": [
859
+ {
860
+ "finish_reason": "stop",
861
+ "message": {
862
+ "content": (
863
+ "I reviewed the repository and here is a long explanation that still "
864
+ "does not include any valid tool call payload for this required turn."
865
+ ),
866
+ "tool_calls": [],
867
+ },
868
+ }
869
+ ]
870
+ }
871
+ anthropic_body = {
872
+ "tools": [{"name": "Edit", "input_schema": {"type": "object"}}],
873
+ }
874
+
875
+ issue = proxy._classify_tool_response_issue(
876
+ openai_resp, anthropic_body, required_tool_choice=True
877
+ )
878
+ self.assertEqual(issue.kind, "required_tool_miss")
879
+
880
+ def test_preflight_flags_repetitive_policy_echo_without_tool_call(self):
881
+ repeated = " (describe/it/expect using vitest" * 24
882
+ openai_resp = {
883
+ "choices": [
884
+ {
885
+ "finish_reason": "stop",
886
+ "message": {
887
+ "content": (
888
+ "- At least 2 new test cases before claiming done. "
889
+ "- Tests must be in test/ following existing patterns."
890
+ f"{repeated}"
891
+ ),
892
+ "tool_calls": [],
893
+ },
894
+ }
895
+ ]
896
+ }
897
+ anthropic_body = {
898
+ "tools": [{"name": "Read", "input_schema": {"type": "object"}}],
899
+ }
900
+
901
+ issue = proxy._classify_tool_response_issue(openai_resp, anthropic_body)
902
+ self.assertEqual(issue.kind, "malformed_payload")
903
+
808
904
  def test_markup_repair_sanitizes_tool_arguments(self):
809
905
  openai_resp = {
810
906
  "choices": [