@miller-tech/uap 2.16.4 → 2.16.6

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": "2.16.4",
3
+ "version": "2.16.6",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11783,6 +11783,19 @@ async def _apply_unexpected_end_turn_guardrail(
11783
11783
  logger.info("GUARDRAIL: skipped unexpected_end_turn retry on finalize turn")
11784
11784
  return openai_resp
11785
11785
 
11786
+ # A tools-stripped breaker turn (STUCK-BREAK HARD / finalize-adjacent) ends
11787
+ # in prose BY DESIGN -- observed live 2026-09-29 08:43:40, the model finally
11788
+ # produced the prose exit and this guardrail treated it as an anomaly and
11789
+ # retried with tool_choice=required, re-coercing the very loop the breaker
11790
+ # exists to end (and burning a full prefill to do it).
11791
+ if monitor.suppress_text_tool_extraction:
11792
+ logger.info(
11793
+ "GUARDRAIL: skipped unexpected_end_turn retry on a tools-stripped "
11794
+ "breaker turn -- the prose exit is the intended outcome, not an "
11795
+ "anomaly to re-coerce"
11796
+ )
11797
+ return openai_resp
11798
+
11786
11799
  if not _is_unexpected_end_turn(openai_resp, anthropic_body):
11787
11800
  return openai_resp
11788
11801
 
@@ -11877,6 +11890,15 @@ async def _apply_malformed_tool_guardrail(
11877
11890
  logger.info("GUARDRAIL: finalize turn clean, no tool call XML detected")
11878
11891
  return openai_resp
11879
11892
 
11893
+ # A tools-stripped breaker turn drops whatever the backend resurrects at
11894
+ # conversion time; repairing the resurrected call's arguments first would
11895
+ # burn a retry prefill perfecting a call that is about to be discarded.
11896
+ if monitor.suppress_text_tool_extraction:
11897
+ logger.info(
11898
+ "GUARDRAIL: skipped malformed-tool retry on a tools-stripped breaker turn"
11899
+ )
11900
+ return openai_resp
11901
+
11880
11902
  working_resp = openai_resp
11881
11903
  repair_count = 0
11882
11904
  if PROXY_TOOL_ARGS_PREFLIGHT and _openai_has_tool_calls(openai_resp):
@@ -12922,6 +12944,41 @@ def openai_to_anthropic_response(
12922
12944
  message = choice.get("message", {})
12923
12945
  finish = choice.get("finish_reason", "stop")
12924
12946
 
12947
+ # Structured resurrection on a tools-stripped turn. The text-markup
12948
+ # suppression above only scrubs tool calls written into the TEXT body --
12949
+ # but the BACKEND can still parse the model's repeated <tool_call> into a
12950
+ # STRUCTURED tool_calls field even with an empty tools list (observed live
12951
+ # 2026-09-29 08:38, tabbyAPI: finish=tool_calls, text_len=0, the identical
12952
+ # ssh call re-served 30+ times at ~9 req/min; the client re-executed it
12953
+ # each pass and the loop never ended). On a suppressed turn there are, by
12954
+ # construction, no tools, so ANY structured call is resurrection: drop it,
12955
+ # force the finish back to prose, and let the fallback below carry the
12956
+ # reply when the model wrote nothing else.
12957
+ if suppress_text_tool_extraction and message.get("tool_calls"):
12958
+ dropped = message.pop("tool_calls") or []
12959
+ if finish in ("tool_calls", "function_call"):
12960
+ choice["finish_reason"] = "stop"
12961
+ finish = "stop"
12962
+ _suppressed_content = message.get("content")
12963
+ if _suppressed_content is None or (
12964
+ isinstance(_suppressed_content, str) and not _suppressed_content.strip()
12965
+ ):
12966
+ message["content"] = STUCK_BREAK_PROSE_FALLBACK
12967
+ logger.warning(
12968
+ "STUCK-BREAK HARD: suppressed STRUCTURED tool_calls from the backend on "
12969
+ "a tools-stripped turn (%d call(s), e.g. %s) -- the turn now ends in "
12970
+ "prose as intended",
12971
+ len(dropped),
12972
+ next(
12973
+ (
12974
+ (c.get("function") or {}).get("name", "?")
12975
+ for c in dropped
12976
+ if isinstance(c, dict)
12977
+ ),
12978
+ "?",
12979
+ ),
12980
+ )
12981
+
12925
12982
  content = []
12926
12983
  # Surface Qwen's <think>...</think> output as Anthropic-style thinking
12927
12984
  # blocks (Anthropic extended-thinking API shape:
@@ -1737,6 +1737,114 @@ class TestMalformedToolGuardrail(unittest.TestCase):
1737
1737
  self.assertEqual(malformed, openai_resp)
1738
1738
  self.assertEqual(len(fake_client.requests), 0)
1739
1739
 
1740
+ def test_guardrails_skip_tools_stripped_breaker_turn(self):
1741
+ # A STUCK-BREAK HARD turn ending in prose is the INTENDED exit, not an
1742
+ # anomaly. Live 2026-09-29 08:43:40 (ses_f18850847): the model finally
1743
+ # produced the prose exit and the unexpected_end_turn guardrail retried
1744
+ # it with tool_choice=required, re-coercing the very loop the breaker
1745
+ # exists to end. Both guardrails must yield on a suppressed turn.
1746
+ monitor = proxy.SessionMonitor(context_window=262144)
1747
+ monitor.suppress_text_tool_extraction = True
1748
+ monitor.tool_turn_phase = "act"
1749
+
1750
+ # Prose end_turn + an active tool loop: exactly the shape the
1751
+ # unexpected_end_turn guardrail would otherwise retry.
1752
+ openai_resp = {
1753
+ "choices": [
1754
+ {
1755
+ "finish_reason": "stop",
1756
+ "message": {
1757
+ "content": "The query keeps failing; I need operator input.",
1758
+ "tool_calls": [],
1759
+ },
1760
+ }
1761
+ ]
1762
+ }
1763
+ # A garbled structured call: exactly the shape the malformed-tool
1764
+ # guardrail would otherwise spend a retry prefill repairing -- on a
1765
+ # turn where conversion drops the call anyway.
1766
+ garbled_resp = {
1767
+ "choices": [
1768
+ {
1769
+ "finish_reason": "tool_calls",
1770
+ "message": {
1771
+ "content": "",
1772
+ "tool_calls": [
1773
+ {
1774
+ "id": "call_1",
1775
+ "type": "function",
1776
+ "function": {
1777
+ "name": "Bash",
1778
+ "arguments": '{"command": "ssh root@db psq',
1779
+ },
1780
+ }
1781
+ ],
1782
+ },
1783
+ }
1784
+ ]
1785
+ }
1786
+ openai_body = {
1787
+ "model": "test",
1788
+ "tool_choice": "auto",
1789
+ "messages": [{"role": "user", "content": "continue"}],
1790
+ }
1791
+ anthropic_body = {
1792
+ "tools": [{"name": "Bash", "input_schema": {"type": "object"}}],
1793
+ "messages": [
1794
+ {"role": "user", "content": "start"},
1795
+ {
1796
+ "role": "assistant",
1797
+ "content": [
1798
+ {
1799
+ "type": "tool_use",
1800
+ "id": "toolu_1",
1801
+ "name": "Bash",
1802
+ "input": {"command": "pwd"},
1803
+ }
1804
+ ],
1805
+ },
1806
+ {
1807
+ "role": "user",
1808
+ "content": [
1809
+ {
1810
+ "type": "tool_result",
1811
+ "tool_use_id": "toolu_1",
1812
+ "content": "ok",
1813
+ }
1814
+ ],
1815
+ },
1816
+ ],
1817
+ }
1818
+ fake_client = _FakeClient([_FakeResponse({"choices": []})])
1819
+ unexpected = asyncio.run(
1820
+ proxy._apply_unexpected_end_turn_guardrail(
1821
+ fake_client,
1822
+ openai_resp,
1823
+ openai_body,
1824
+ anthropic_body,
1825
+ monitor,
1826
+ "session-stuck-break",
1827
+ )
1828
+ )
1829
+ malformed = asyncio.run(
1830
+ proxy._apply_malformed_tool_guardrail(
1831
+ fake_client,
1832
+ garbled_resp,
1833
+ openai_body,
1834
+ anthropic_body,
1835
+ monitor,
1836
+ "session-stuck-break",
1837
+ )
1838
+ )
1839
+ self.assertEqual(unexpected, openai_resp)
1840
+ self.assertEqual(malformed, garbled_resp)
1841
+ self.assertEqual(
1842
+ malformed["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
1843
+ '{"command": "ssh root@db psq',
1844
+ "a suppressed turn must not repair a call it is about to drop",
1845
+ )
1846
+ self.assertEqual(len(fake_client.requests), 0)
1847
+
1740
1848
  def test_unexpected_end_turn_guardrail_retries_review_auto_turn_in_active_loop(self):
1741
1849
  monitor = proxy.SessionMonitor(context_window=262144)
1742
1850
  monitor.tool_turn_phase = "review"
@@ -413,3 +413,80 @@ class TestSuppressedTurnIsBuffered(unittest.TestCase):
413
413
  self.skipTest("ambient proxy config already forces buffering here")
414
414
  self.assertFalse(proxy._should_buffer_turn(True, body, openai_body, self._monitor(False)))
415
415
  self.assertTrue(proxy._should_buffer_turn(True, body, openai_body, self._monitor(True)))
416
+
417
+
418
+ class TestSuppressedStructuredToolCalls(unittest.TestCase):
419
+ """The STRUCTURED half of the hard break (live incident 2026-09-29 08:38).
420
+
421
+ The text-markup suppression scrubs tool calls written into the text body,
422
+ but the BACKEND can still parse the model's repeated markup into a
423
+ STRUCTURED tool_calls field on a tools-stripped turn -- observed on tabbyAPI
424
+ as finish=tool_calls with an EMPTY text body, re-served 30+ times at ~9
425
+ req/min. The conversion promoted each one straight back to tool_use, the
426
+ client re-executed the identical ssh call, and the loop ran for hours. On a
427
+ suppressed turn there are by construction no tools, so ANY structured call
428
+ is resurrection and must be dropped.
429
+ """
430
+
431
+ @staticmethod
432
+ def _resp(text="", finish="tool_calls"):
433
+ return {
434
+ "choices": [
435
+ {
436
+ "finish_reason": finish,
437
+ "message": {
438
+ "role": "assistant",
439
+ "content": text,
440
+ "tool_calls": [
441
+ {
442
+ "id": "call_1",
443
+ "type": "function",
444
+ "function": {
445
+ "name": "bash",
446
+ "arguments": '{"command": "ssh root@db psql"}',
447
+ },
448
+ }
449
+ ],
450
+ },
451
+ }
452
+ ],
453
+ "usage": {"prompt_tokens": 100, "completion_tokens": 0},
454
+ }
455
+
456
+ @staticmethod
457
+ def _text(out):
458
+ return "".join(b.get("text") or "" for b in out.get("content") or [])
459
+
460
+ def test_resurrected_call_is_dropped_and_turn_is_a_prose_end_turn(self):
461
+ out = proxy.openai_to_anthropic_response(
462
+ self._resp(), "qwen", suppress_text_tool_extraction=True
463
+ )
464
+ self.assertEqual(out["stop_reason"], "end_turn")
465
+ types = [b.get("type") for b in out["content"]]
466
+ self.assertNotIn("tool_use", types, "a suppressed turn must not carry tool_use")
467
+
468
+ def test_empty_body_gets_fallback_prose_not_a_blank_reply(self):
469
+ # The live incident shape: text_len=0, everything in tool_calls.
470
+ out = proxy.openai_to_anthropic_response(
471
+ self._resp(text=""), "qwen", suppress_text_tool_extraction=True
472
+ )
473
+ joined = self._text(out)
474
+ self.assertIn("repeated the same tool call", joined)
475
+ self.assertNotIn("ssh root@db psql", joined, "dropped args must not leak")
476
+
477
+ def test_model_prose_is_kept_when_the_resurrected_call_is_dropped(self):
478
+ out = proxy.openai_to_anthropic_response(
479
+ self._resp(text="The query keeps failing; I need operator input."),
480
+ "qwen",
481
+ suppress_text_tool_extraction=True,
482
+ )
483
+ self.assertIn("The query keeps failing", self._text(out))
484
+
485
+ def test_normal_turns_still_promote_structured_calls(self):
486
+ # The suppression must not leak onto the normal path, where serving a
487
+ # structured call is the whole point of the conversion.
488
+ out = proxy.openai_to_anthropic_response(
489
+ self._resp(), "qwen", suppress_text_tool_extraction=False
490
+ )
491
+ self.assertEqual(out["stop_reason"], "tool_use")
492
+ self.assertTrue(any(b.get("type") == "tool_use" for b in out["content"]))