@miller-tech/uap 1.20.50 → 1.20.51
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
|
@@ -3288,7 +3288,11 @@ def _resolve_state_machine_tool_choice(
|
|
|
3288
3288
|
return None, "unknown_phase"
|
|
3289
3289
|
|
|
3290
3290
|
|
|
3291
|
-
def _maybe_inject_recon_convergence(
|
|
3291
|
+
def _maybe_inject_recon_convergence(
|
|
3292
|
+
openai_body: dict,
|
|
3293
|
+
monitor: "SessionMonitor",
|
|
3294
|
+
full_tools: list[dict] | None = None,
|
|
3295
|
+
) -> None:
|
|
3292
3296
|
"""Nudge a session stuck in prolonged exploration toward its deliverable.
|
|
3293
3297
|
|
|
3294
3298
|
Fires when `consecutive_no_write_turns` crosses
|
|
@@ -3298,6 +3302,13 @@ def _maybe_inject_recon_convergence(openai_body: dict, monitor: "SessionMonitor"
|
|
|
3298
3302
|
of turns and never converging to the synthesis/write step. Two
|
|
3299
3303
|
escalation tiers: a firm "switch to synthesis" directive, then a hard
|
|
3300
3304
|
"STOP, write it now" once the streak is 2x over threshold.
|
|
3305
|
+
|
|
3306
|
+
`full_tools` is the request's tool list *before* `_narrow_tools_for_request`
|
|
3307
|
+
pruned it. When the directive fires, any write/deliverable tool that
|
|
3308
|
+
narrowing dropped is re-injected into `openai_body["tools"]` — narrowing
|
|
3309
|
+
scores tools against the (exploration-heavy) recon prompt and runs before
|
|
3310
|
+
this guardrail, so it routinely strips the very write tool the directive
|
|
3311
|
+
tells the model to use, leaving the directive impossible to satisfy.
|
|
3301
3312
|
"""
|
|
3302
3313
|
if PROXY_RECON_CONVERGENCE_THRESHOLD <= 0:
|
|
3303
3314
|
return
|
|
@@ -3327,9 +3338,28 @@ def _maybe_inject_recon_convergence(openai_body: dict, monitor: "SessionMonitor"
|
|
|
3327
3338
|
msgs = openai_body.get("messages", [])
|
|
3328
3339
|
msgs.append({"role": "user", "content": directive})
|
|
3329
3340
|
openai_body["messages"] = msgs
|
|
3341
|
+
|
|
3342
|
+
# Re-inject any write/deliverable tool that narrowing dropped, so the
|
|
3343
|
+
# "write your deliverable" directive is actually satisfiable. Without
|
|
3344
|
+
# this the model is told to write but has no write tool to call, picks
|
|
3345
|
+
# another read tool, and the streak climbs unbounded.
|
|
3346
|
+
restored: list[str] = []
|
|
3347
|
+
if full_tools:
|
|
3348
|
+
present = {
|
|
3349
|
+
(t.get("function", {}).get("name", "") or "").lower()
|
|
3350
|
+
for t in openai_body.get("tools", [])
|
|
3351
|
+
}
|
|
3352
|
+
for tool in full_tools:
|
|
3353
|
+
name = (tool.get("function", {}).get("name", "") or "")
|
|
3354
|
+
if name.lower() in _WRITE_TOOL_CLASS and name.lower() not in present:
|
|
3355
|
+
openai_body.setdefault("tools", []).append(tool)
|
|
3356
|
+
present.add(name.lower())
|
|
3357
|
+
restored.append(name)
|
|
3358
|
+
|
|
3330
3359
|
logger.warning(
|
|
3331
|
-
"RECON CONVERGENCE: injected %s directive (no_write_streak=%d, ctx=%.0f
|
|
3332
|
-
|
|
3360
|
+
"RECON CONVERGENCE: injected %s directive (no_write_streak=%d, ctx=%.0f%%, "
|
|
3361
|
+
"restored_write_tools=%s)",
|
|
3362
|
+
tier, streak, util * 100, restored or "none",
|
|
3333
3363
|
)
|
|
3334
3364
|
|
|
3335
3365
|
|
|
@@ -3575,10 +3605,14 @@ def build_openai_request(
|
|
|
3575
3605
|
)
|
|
3576
3606
|
|
|
3577
3607
|
# Convert Anthropic tools to OpenAI function-calling tools
|
|
3608
|
+
full_openai_tools: list[dict] = []
|
|
3578
3609
|
if has_tools:
|
|
3579
3610
|
openai_body["tools"] = _convert_anthropic_tools_to_openai(
|
|
3580
3611
|
anthropic_body.get("tools", [])
|
|
3581
3612
|
)
|
|
3613
|
+
# Keep the full (pre-narrowing) list so the recon-convergence
|
|
3614
|
+
# guardrail can restore a write tool that narrowing dropped.
|
|
3615
|
+
full_openai_tools = openai_body["tools"]
|
|
3582
3616
|
openai_body["tools"] = _narrow_tools_for_request(
|
|
3583
3617
|
anthropic_body, openai_body["tools"]
|
|
3584
3618
|
)
|
|
@@ -3842,8 +3876,9 @@ def build_openai_request(
|
|
|
3842
3876
|
|
|
3843
3877
|
# Recon-convergence guardrail (B1) — runs on every built request so a
|
|
3844
3878
|
# session wandering in exploration without producing a write is nudged
|
|
3845
|
-
# toward its deliverable regardless of tool-turn phase.
|
|
3846
|
-
|
|
3879
|
+
# toward its deliverable regardless of tool-turn phase. Passed the full
|
|
3880
|
+
# pre-narrowing toolset so it can restore a dropped write tool.
|
|
3881
|
+
_maybe_inject_recon_convergence(openai_body, monitor, full_openai_tools)
|
|
3847
3882
|
|
|
3848
3883
|
return openai_body
|
|
3849
3884
|
|
|
@@ -5479,6 +5479,84 @@ class TestReconConvergence(unittest.TestCase):
|
|
|
5479
5479
|
proxy._maybe_inject_recon_convergence(body, m)
|
|
5480
5480
|
self.assertEqual(len(body["messages"]), 1)
|
|
5481
5481
|
|
|
5482
|
+
@staticmethod
|
|
5483
|
+
def _tool(name: str) -> dict:
|
|
5484
|
+
return {"type": "function", "function": {"name": name, "description": f"{name} tool"}}
|
|
5485
|
+
|
|
5486
|
+
def test_dropped_write_tool_is_restored_when_directive_fires(self):
|
|
5487
|
+
"""The core fix: if narrowing left no write tool in the request,
|
|
5488
|
+
a firing directive re-injects it from the full pre-narrowing set."""
|
|
5489
|
+
proxy.PROXY_RECON_CONVERGENCE_THRESHOLD = 40
|
|
5490
|
+
m = proxy.SessionMonitor(context_window=131072)
|
|
5491
|
+
m.consecutive_no_write_turns = 45
|
|
5492
|
+
# narrowed toolset — exploration tools only, no write tool
|
|
5493
|
+
body = {
|
|
5494
|
+
"messages": [{"role": "user", "content": "go"}],
|
|
5495
|
+
"tools": [self._tool("Read"), self._tool("Grep"), self._tool("Bash")],
|
|
5496
|
+
}
|
|
5497
|
+
# full pre-narrowing set DID include a write tool
|
|
5498
|
+
full = body["tools"] + [self._tool("Edit")]
|
|
5499
|
+
proxy._maybe_inject_recon_convergence(body, m, full)
|
|
5500
|
+
names = [t["function"]["name"] for t in body["tools"]]
|
|
5501
|
+
self.assertIn("Edit", names)
|
|
5502
|
+
|
|
5503
|
+
def test_present_write_tool_not_duplicated(self):
|
|
5504
|
+
"""If a write tool already survived narrowing, it is not added twice."""
|
|
5505
|
+
proxy.PROXY_RECON_CONVERGENCE_THRESHOLD = 40
|
|
5506
|
+
m = proxy.SessionMonitor(context_window=131072)
|
|
5507
|
+
m.consecutive_no_write_turns = 45
|
|
5508
|
+
body = {
|
|
5509
|
+
"messages": [{"role": "user", "content": "go"}],
|
|
5510
|
+
"tools": [self._tool("Read"), self._tool("Edit")],
|
|
5511
|
+
}
|
|
5512
|
+
full = list(body["tools"])
|
|
5513
|
+
proxy._maybe_inject_recon_convergence(body, m, full)
|
|
5514
|
+
names = [t["function"]["name"] for t in body["tools"]]
|
|
5515
|
+
self.assertEqual(names.count("Edit"), 1)
|
|
5516
|
+
|
|
5517
|
+
def test_no_write_tool_anywhere_is_safe(self):
|
|
5518
|
+
"""A recon agent whose toolset has no write tool at all: nothing to
|
|
5519
|
+
restore, no crash."""
|
|
5520
|
+
proxy.PROXY_RECON_CONVERGENCE_THRESHOLD = 40
|
|
5521
|
+
m = proxy.SessionMonitor(context_window=131072)
|
|
5522
|
+
m.consecutive_no_write_turns = 45
|
|
5523
|
+
body = {
|
|
5524
|
+
"messages": [{"role": "user", "content": "go"}],
|
|
5525
|
+
"tools": [self._tool("Read"), self._tool("Bash")],
|
|
5526
|
+
}
|
|
5527
|
+
proxy._maybe_inject_recon_convergence(body, m, list(body["tools"]))
|
|
5528
|
+
names = [t["function"]["name"] for t in body["tools"]]
|
|
5529
|
+
self.assertEqual(names, ["Read", "Bash"])
|
|
5530
|
+
|
|
5531
|
+
def test_full_tools_omitted_is_safe(self):
|
|
5532
|
+
"""Called without full_tools (default None) — directive still fires,
|
|
5533
|
+
no tool restoration attempted, no crash."""
|
|
5534
|
+
proxy.PROXY_RECON_CONVERGENCE_THRESHOLD = 40
|
|
5535
|
+
m = proxy.SessionMonitor(context_window=131072)
|
|
5536
|
+
m.consecutive_no_write_turns = 45
|
|
5537
|
+
body = {
|
|
5538
|
+
"messages": [{"role": "user", "content": "go"}],
|
|
5539
|
+
"tools": [self._tool("Read")],
|
|
5540
|
+
}
|
|
5541
|
+
proxy._maybe_inject_recon_convergence(body, m)
|
|
5542
|
+
self.assertEqual(len(body["messages"]), 2)
|
|
5543
|
+
self.assertEqual([t["function"]["name"] for t in body["tools"]], ["Read"])
|
|
5544
|
+
|
|
5545
|
+
def test_no_restore_below_threshold(self):
|
|
5546
|
+
"""Below threshold the directive does not fire, so no write tool is
|
|
5547
|
+
restored even if narrowing dropped one."""
|
|
5548
|
+
proxy.PROXY_RECON_CONVERGENCE_THRESHOLD = 40
|
|
5549
|
+
m = proxy.SessionMonitor(context_window=131072)
|
|
5550
|
+
m.consecutive_no_write_turns = 39
|
|
5551
|
+
body = {
|
|
5552
|
+
"messages": [{"role": "user", "content": "go"}],
|
|
5553
|
+
"tools": [self._tool("Read")],
|
|
5554
|
+
}
|
|
5555
|
+
full = body["tools"] + [self._tool("Write")]
|
|
5556
|
+
proxy._maybe_inject_recon_convergence(body, m, full)
|
|
5557
|
+
names = [t["function"]["name"] for t in body["tools"]]
|
|
5558
|
+
self.assertEqual(names, ["Read"])
|
|
5559
|
+
|
|
5482
5560
|
|
|
5483
5561
|
class TestPrunerRework(unittest.TestCase):
|
|
5484
5562
|
"""Tests for the reworked context pruner (B2 + B3): contiguous
|