@miller-tech/uap 1.14.1 → 1.15.0
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
|
@@ -203,6 +203,20 @@ PROXY_SESSION_CONTAMINATION_KEEP_LAST = int(
|
|
|
203
203
|
PROXY_AGENTIC_SUPPLEMENT_MODE = (
|
|
204
204
|
os.environ.get("PROXY_AGENTIC_SUPPLEMENT_MODE", "clean").strip().lower()
|
|
205
205
|
)
|
|
206
|
+
PROXY_ANALYSIS_ONLY_ROUTE = os.environ.get(
|
|
207
|
+
"PROXY_ANALYSIS_ONLY_ROUTE", "off"
|
|
208
|
+
).lower() not in {
|
|
209
|
+
"0",
|
|
210
|
+
"false",
|
|
211
|
+
"off",
|
|
212
|
+
"no",
|
|
213
|
+
}
|
|
214
|
+
PROXY_ANALYSIS_ONLY_MIN_TOOLS = int(
|
|
215
|
+
os.environ.get("PROXY_ANALYSIS_ONLY_MIN_TOOLS", "12")
|
|
216
|
+
)
|
|
217
|
+
PROXY_ANALYSIS_ONLY_MAX_MESSAGES = int(
|
|
218
|
+
os.environ.get("PROXY_ANALYSIS_ONLY_MAX_MESSAGES", "2")
|
|
219
|
+
)
|
|
206
220
|
|
|
207
221
|
# ---------------------------------------------------------------------------
|
|
208
222
|
# Logging
|
|
@@ -549,8 +563,9 @@ def estimate_total_tokens(anthropic_body: dict) -> int:
|
|
|
549
563
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
550
564
|
tokens += estimate_tokens(block.get("text", ""))
|
|
551
565
|
|
|
552
|
-
# Agentic supplement tokens (
|
|
553
|
-
|
|
566
|
+
# Agentic supplement tokens (only when tool mode is active)
|
|
567
|
+
if _has_tool_definitions(anthropic_body):
|
|
568
|
+
tokens += estimate_tokens(_AGENTIC_SYSTEM_SUPPLEMENT)
|
|
554
569
|
|
|
555
570
|
# Messages
|
|
556
571
|
for msg in anthropic_body.get("messages", []):
|
|
@@ -600,7 +615,8 @@ def prune_conversation(
|
|
|
600
615
|
for block in system:
|
|
601
616
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
602
617
|
overhead_tokens += estimate_tokens(block.get("text", ""))
|
|
603
|
-
|
|
618
|
+
if _has_tool_definitions(anthropic_body):
|
|
619
|
+
overhead_tokens += estimate_tokens(_AGENTIC_SYSTEM_SUPPLEMENT)
|
|
604
620
|
tools = anthropic_body.get("tools", [])
|
|
605
621
|
if tools:
|
|
606
622
|
overhead_tokens += estimate_tokens(json.dumps(tools))
|
|
@@ -768,7 +784,7 @@ async def lifespan(app: FastAPI):
|
|
|
768
784
|
_resolve_prune_target_fraction() * 100,
|
|
769
785
|
)
|
|
770
786
|
logger.info(
|
|
771
|
-
"Guardrails: malformed=%s stream_strict=%s force_non_stream=%s tool_narrowing=%s thinking_off_on_tools=%s contamination_breaker=%s(%d)",
|
|
787
|
+
"Guardrails: malformed=%s stream_strict=%s force_non_stream=%s tool_narrowing=%s thinking_off_on_tools=%s contamination_breaker=%s(%d) analysis_only_route=%s(min_tools=%d,max_msgs=%d)",
|
|
772
788
|
PROXY_MALFORMED_TOOL_GUARDRAIL,
|
|
773
789
|
PROXY_MALFORMED_TOOL_STREAM_STRICT,
|
|
774
790
|
PROXY_FORCE_NON_STREAM,
|
|
@@ -776,6 +792,9 @@ async def lifespan(app: FastAPI):
|
|
|
776
792
|
PROXY_DISABLE_THINKING_ON_TOOL_TURNS,
|
|
777
793
|
PROXY_SESSION_CONTAMINATION_BREAKER,
|
|
778
794
|
PROXY_SESSION_CONTAMINATION_THRESHOLD,
|
|
795
|
+
PROXY_ANALYSIS_ONLY_ROUTE,
|
|
796
|
+
PROXY_ANALYSIS_ONLY_MIN_TOOLS,
|
|
797
|
+
PROXY_ANALYSIS_ONLY_MAX_MESSAGES,
|
|
779
798
|
)
|
|
780
799
|
|
|
781
800
|
yield
|
|
@@ -879,6 +898,112 @@ def _extract_text(content) -> str:
|
|
|
879
898
|
return str(content)
|
|
880
899
|
|
|
881
900
|
|
|
901
|
+
def _has_tool_definitions(anthropic_body: dict) -> bool:
|
|
902
|
+
tools = anthropic_body.get("tools")
|
|
903
|
+
return isinstance(tools, list) and len(tools) > 0
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _message_has_tool_result(content) -> bool:
|
|
907
|
+
return isinstance(content, list) and any(
|
|
908
|
+
isinstance(block, dict) and block.get("type") == "tool_result"
|
|
909
|
+
for block in content
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _last_user_text(anthropic_body: dict) -> str:
|
|
914
|
+
for msg in reversed(anthropic_body.get("messages", [])):
|
|
915
|
+
if msg.get("role") == "user":
|
|
916
|
+
return _extract_text(msg.get("content", "")).strip().lower()
|
|
917
|
+
return ""
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _is_analysis_only_prompt(text: str) -> bool:
|
|
921
|
+
if not text:
|
|
922
|
+
return False
|
|
923
|
+
|
|
924
|
+
analysis_markers = (
|
|
925
|
+
"analy",
|
|
926
|
+
"review",
|
|
927
|
+
"audit",
|
|
928
|
+
"summar",
|
|
929
|
+
"explain",
|
|
930
|
+
"plan",
|
|
931
|
+
"recommend",
|
|
932
|
+
"assess",
|
|
933
|
+
"compare",
|
|
934
|
+
"investigate",
|
|
935
|
+
"diagnose",
|
|
936
|
+
)
|
|
937
|
+
action_markers = (
|
|
938
|
+
"fix",
|
|
939
|
+
"edit",
|
|
940
|
+
"write",
|
|
941
|
+
"create",
|
|
942
|
+
"implement",
|
|
943
|
+
"patch",
|
|
944
|
+
"change",
|
|
945
|
+
"update",
|
|
946
|
+
"run ",
|
|
947
|
+
"execute",
|
|
948
|
+
"command",
|
|
949
|
+
"use tool",
|
|
950
|
+
"call tool",
|
|
951
|
+
"apply",
|
|
952
|
+
"commit",
|
|
953
|
+
"push",
|
|
954
|
+
"merge",
|
|
955
|
+
"publish",
|
|
956
|
+
"deploy",
|
|
957
|
+
"test",
|
|
958
|
+
"build",
|
|
959
|
+
"refactor",
|
|
960
|
+
"rename",
|
|
961
|
+
"delete",
|
|
962
|
+
"install",
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
has_analysis = any(marker in text for marker in analysis_markers)
|
|
966
|
+
has_action = any(marker in text for marker in action_markers)
|
|
967
|
+
return has_analysis and not has_action
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _should_route_analysis_without_tools(anthropic_body: dict) -> bool:
|
|
971
|
+
if not PROXY_ANALYSIS_ONLY_ROUTE:
|
|
972
|
+
return False
|
|
973
|
+
|
|
974
|
+
tools = anthropic_body.get("tools")
|
|
975
|
+
if not isinstance(tools, list) or len(tools) < max(
|
|
976
|
+
1, PROXY_ANALYSIS_ONLY_MIN_TOOLS
|
|
977
|
+
):
|
|
978
|
+
return False
|
|
979
|
+
|
|
980
|
+
messages = anthropic_body.get("messages", [])
|
|
981
|
+
if not isinstance(messages, list) or not messages:
|
|
982
|
+
return False
|
|
983
|
+
|
|
984
|
+
if len(messages) > max(1, PROXY_ANALYSIS_ONLY_MAX_MESSAGES):
|
|
985
|
+
return False
|
|
986
|
+
|
|
987
|
+
if any(msg.get("role") == "assistant" for msg in messages):
|
|
988
|
+
return False
|
|
989
|
+
|
|
990
|
+
if any(_message_has_tool_result(msg.get("content")) for msg in messages):
|
|
991
|
+
return False
|
|
992
|
+
|
|
993
|
+
return _is_analysis_only_prompt(_last_user_text(anthropic_body))
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _maybe_route_analysis_without_tools(anthropic_body: dict) -> tuple[dict, int]:
|
|
997
|
+
if not _should_route_analysis_without_tools(anthropic_body):
|
|
998
|
+
return anthropic_body, 0
|
|
999
|
+
|
|
1000
|
+
tools = anthropic_body.get("tools")
|
|
1001
|
+
removed = len(tools) if isinstance(tools, list) else 0
|
|
1002
|
+
updated = dict(anthropic_body)
|
|
1003
|
+
updated.pop("tools", None)
|
|
1004
|
+
return updated, removed
|
|
1005
|
+
|
|
1006
|
+
|
|
882
1007
|
_AGENTIC_SYSTEM_SUPPLEMENT_LEGACY = (
|
|
883
1008
|
"\n\n<agentic-protocol>\n"
|
|
884
1009
|
"You are operating in an agentic coding loop with tool access. Follow these rules:\n"
|
|
@@ -1076,19 +1201,24 @@ def build_openai_request(anthropic_body: dict, monitor: SessionMonitor) -> dict:
|
|
|
1076
1201
|
"stream": anthropic_body.get("stream", False),
|
|
1077
1202
|
}
|
|
1078
1203
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1204
|
+
has_tools = _has_tool_definitions(anthropic_body)
|
|
1205
|
+
|
|
1206
|
+
# Inject agentic protocol instructions only for tool-enabled turns.
|
|
1207
|
+
if has_tools:
|
|
1208
|
+
if (
|
|
1209
|
+
openai_body["messages"]
|
|
1210
|
+
and openai_body["messages"][0].get("role") == "system"
|
|
1211
|
+
):
|
|
1212
|
+
openai_body["messages"][0]["content"] += _AGENTIC_SYSTEM_SUPPLEMENT
|
|
1213
|
+
else:
|
|
1214
|
+
# No system message from the client; inject one.
|
|
1215
|
+
openai_body["messages"].insert(
|
|
1216
|
+
0,
|
|
1217
|
+
{
|
|
1218
|
+
"role": "system",
|
|
1219
|
+
"content": _AGENTIC_SYSTEM_SUPPLEMENT.strip(),
|
|
1220
|
+
},
|
|
1221
|
+
)
|
|
1092
1222
|
|
|
1093
1223
|
if "max_tokens" in anthropic_body:
|
|
1094
1224
|
# Enforce configurable minimum floor for thinking mode: model needs
|
|
@@ -1137,7 +1267,7 @@ def build_openai_request(anthropic_body: dict, monitor: SessionMonitor) -> dict:
|
|
|
1137
1267
|
openai_body["stop"] = anthropic_body["stop_sequences"]
|
|
1138
1268
|
|
|
1139
1269
|
# Convert Anthropic tools to OpenAI function-calling tools
|
|
1140
|
-
if
|
|
1270
|
+
if has_tools:
|
|
1141
1271
|
openai_body["tools"] = _convert_anthropic_tools_to_openai(
|
|
1142
1272
|
anthropic_body.get("tools", [])
|
|
1143
1273
|
)
|
|
@@ -2200,6 +2330,14 @@ async def messages(request: Request):
|
|
|
2200
2330
|
last_session_id = session_id
|
|
2201
2331
|
|
|
2202
2332
|
body = _maybe_apply_session_contamination_breaker(body, monitor, session_id)
|
|
2333
|
+
body, analysis_tools_removed = _maybe_route_analysis_without_tools(body)
|
|
2334
|
+
if analysis_tools_removed > 0:
|
|
2335
|
+
monitor.consecutive_forced_count = 0
|
|
2336
|
+
monitor.no_progress_streak = 0
|
|
2337
|
+
logger.info(
|
|
2338
|
+
"ANALYSIS ROUTE: disabled %d tools for analysis-only prompt",
|
|
2339
|
+
analysis_tools_removed,
|
|
2340
|
+
)
|
|
2203
2341
|
|
|
2204
2342
|
# Debug: log request summary
|
|
2205
2343
|
n_messages = len(body.get("messages", []))
|
|
@@ -483,6 +483,82 @@ class TestToolTurnControls(unittest.TestCase):
|
|
|
483
483
|
finally:
|
|
484
484
|
setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", old_disable)
|
|
485
485
|
|
|
486
|
+
def test_no_tools_does_not_inject_agentic_system_message(self):
|
|
487
|
+
body = {
|
|
488
|
+
"model": "test",
|
|
489
|
+
"messages": [{"role": "user", "content": "analyze architecture"}],
|
|
490
|
+
}
|
|
491
|
+
openai = proxy.build_openai_request(
|
|
492
|
+
body, proxy.SessionMonitor(context_window=262144)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
self.assertEqual(openai["messages"][0]["role"], "user")
|
|
496
|
+
self.assertNotIn("tools", openai)
|
|
497
|
+
|
|
498
|
+
def test_analysis_only_route_removes_tools(self):
|
|
499
|
+
old_route = getattr(proxy, "PROXY_ANALYSIS_ONLY_ROUTE")
|
|
500
|
+
old_min_tools = getattr(proxy, "PROXY_ANALYSIS_ONLY_MIN_TOOLS")
|
|
501
|
+
old_max_messages = getattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES")
|
|
502
|
+
try:
|
|
503
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_ROUTE", True)
|
|
504
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MIN_TOOLS", 4)
|
|
505
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES", 2)
|
|
506
|
+
|
|
507
|
+
body = {
|
|
508
|
+
"messages": [
|
|
509
|
+
{
|
|
510
|
+
"role": "user",
|
|
511
|
+
"content": "analyze lifecycle and plan options to improve performance and compliance",
|
|
512
|
+
}
|
|
513
|
+
],
|
|
514
|
+
"tools": [
|
|
515
|
+
{"name": "Read", "input_schema": {"type": "object"}},
|
|
516
|
+
{"name": "Edit", "input_schema": {"type": "object"}},
|
|
517
|
+
{"name": "Write", "input_schema": {"type": "object"}},
|
|
518
|
+
{"name": "Bash", "input_schema": {"type": "object"}},
|
|
519
|
+
],
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
updated, removed = proxy._maybe_route_analysis_without_tools(body)
|
|
523
|
+
self.assertEqual(removed, 4)
|
|
524
|
+
self.assertNotIn("tools", updated)
|
|
525
|
+
finally:
|
|
526
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_ROUTE", old_route)
|
|
527
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MIN_TOOLS", old_min_tools)
|
|
528
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES", old_max_messages)
|
|
529
|
+
|
|
530
|
+
def test_analysis_only_route_keeps_tools_for_action_prompt(self):
|
|
531
|
+
old_route = getattr(proxy, "PROXY_ANALYSIS_ONLY_ROUTE")
|
|
532
|
+
old_min_tools = getattr(proxy, "PROXY_ANALYSIS_ONLY_MIN_TOOLS")
|
|
533
|
+
old_max_messages = getattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES")
|
|
534
|
+
try:
|
|
535
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_ROUTE", True)
|
|
536
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MIN_TOOLS", 4)
|
|
537
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES", 2)
|
|
538
|
+
|
|
539
|
+
body = {
|
|
540
|
+
"messages": [
|
|
541
|
+
{
|
|
542
|
+
"role": "user",
|
|
543
|
+
"content": "analyze failing run and fix the bug",
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
"tools": [
|
|
547
|
+
{"name": "Read", "input_schema": {"type": "object"}},
|
|
548
|
+
{"name": "Edit", "input_schema": {"type": "object"}},
|
|
549
|
+
{"name": "Write", "input_schema": {"type": "object"}},
|
|
550
|
+
{"name": "Bash", "input_schema": {"type": "object"}},
|
|
551
|
+
],
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
updated, removed = proxy._maybe_route_analysis_without_tools(body)
|
|
555
|
+
self.assertEqual(removed, 0)
|
|
556
|
+
self.assertIn("tools", updated)
|
|
557
|
+
finally:
|
|
558
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_ROUTE", old_route)
|
|
559
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MIN_TOOLS", old_min_tools)
|
|
560
|
+
setattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES", old_max_messages)
|
|
561
|
+
|
|
486
562
|
|
|
487
563
|
class TestSessionContaminationBreaker(unittest.TestCase):
|
|
488
564
|
def test_contamination_breaker_trims_and_resets_streak(self):
|