@miller-tech/uap 1.119.1 → 1.119.2

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.119.1",
3
+ "version": "1.119.2",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -380,6 +380,13 @@ _WRITE_TOOL_CLASS = frozenset({
380
380
  # directives into a loop that was mid-build — observed live at
381
381
  # no_write_streak=62 with the model then emitting files as plain text.
382
382
  "write_file", "edit_file", "save_file",
383
+ # A: `deliver` is the ONLY write path when delivery-enforcement gates
384
+ # direct Edit/Write. Counting it as a write means (a) a deliver call
385
+ # resets the no-write streak instead of looking like more exploration,
386
+ # and (b) the recon-convergence restore loop re-injects `deliver` when
387
+ # narrowing dropped it, so a gated "route through deliver" directive is
388
+ # actually satisfiable.
389
+ "deliver",
383
390
  })
384
391
 
385
392
  # Open-ended exploration tools the agent uses to make a DIFFERENT move once a
@@ -614,6 +621,15 @@ def _maybe_normalize_toolcall_paths(anthropic_resp: dict, request_body: dict) ->
614
621
  PROXY_RECON_CONVERGENCE_THRESHOLD = int(
615
622
  os.environ.get("PROXY_RECON_CONVERGENCE_THRESHOLD", "40")
616
623
  )
624
+ # Fix C: the hard tier historically fired at 2x the base threshold (80 turns
625
+ # at the default 40) -- far too late; the model burns ~40 extra turns reading
626
+ # before the stronger "STOP, write now / release tool_choice" directive kicks
627
+ # in. This multiplier makes the hard-tier onset configurable. 1.5x (60 turns)
628
+ # escalates sooner while still leaving the firm tier a working window. Clamped
629
+ # to >= 1.0 so the hard tier can never precede the firm tier.
630
+ PROXY_RECON_HARD_MULTIPLIER = max(1.0, float(
631
+ os.environ.get("PROXY_RECON_HARD_MULTIPLIER", "1.5")
632
+ ))
617
633
  # Fix E: the recon hard tier (streak >= 2x threshold) fires a directive + flips
618
634
  # tool_choice to 'auto', but `consecutive_no_write_turns` resets to 0 whenever
619
635
  # the model emits any write tool — so a loop that periodically writes sawtooths
@@ -4304,11 +4320,24 @@ def _resolve_state_machine_tool_choice(
4304
4320
 
4305
4321
 
4306
4322
  def _writes_are_gated(openai_body: dict) -> bool:
4307
- """True when recent tool results show the harness is BLOCKING direct writes
4308
- (delivery-enforcement), so a "write the file" directive is futile and the
4309
- model must route through the deliver tool instead. #1: aligns the recon
4310
- directive with the gate to break the read-forever / can't-write deadlock."""
4311
- for m in (openai_body.get("messages") or [])[-8:]:
4323
+ """True when the harness is BLOCKING direct writes (delivery-enforcement),
4324
+ so a "write the file" directive is futile and the model must route through
4325
+ the deliver tool instead. Aligns the recon directive with the gate to break
4326
+ the read-forever / can't-write deadlock.
4327
+
4328
+ Two detection paths:
4329
+ * reactive -- a recent tool_result shows a write was actually blocked
4330
+ (last 8 messages); and
4331
+ * proactive (A) -- the harness gate banner is present anywhere in the
4332
+ context (system / injected-user prompt). A session stuck reading
4333
+ forever NEVER attempts a doomed write, so the reactive path never
4334
+ fires and the deliver redirect never triggers -- the loop is
4335
+ permanent. The proactive path fires the redirect on turn 1 of the
4336
+ streak. Phrases matched are stable harness strings, not model output.
4337
+ """
4338
+ msgs = openai_body.get("messages") or []
4339
+ # Reactive: a recent turn was actually blocked by the gate.
4340
+ for m in msgs[-8:]:
4312
4341
  c = m.get("content")
4313
4342
  text = c if isinstance(c, str) else (json.dumps(c) if c else "")
4314
4343
  low = text.lower()
@@ -4316,6 +4345,24 @@ def _writes_are_gated(openai_body: dict) -> bool:
4316
4345
  "blocked" in low and "deliver" in low and "tool" in low
4317
4346
  ):
4318
4347
  return True
4348
+ # Proactive (A): the gate banner is present in context (esp. the system /
4349
+ # UserPromptSubmit-injected prompt announcing "route through deliver").
4350
+ # Bounded scan: the banner lives in the system prompt(s) or the recent
4351
+ # window, never deep in history -- so we only serialize/lower those, not
4352
+ # the whole (66k-token) transcript on this stuck-path hot request.
4353
+ system_msgs = [m for m in msgs if m.get("role") == "system"]
4354
+ for m in system_msgs + msgs[-8:]:
4355
+ if m.get("role") not in ("system", "user"):
4356
+ continue
4357
+ c = m.get("content")
4358
+ text = c if isinstance(c, str) else (json.dumps(c) if c else "")
4359
+ low = text.lower()
4360
+ if (
4361
+ "gated and will be blocked" in low
4362
+ or "route through deliver" in low
4363
+ or ("direct edit/write" in low and "gated" in low)
4364
+ ):
4365
+ return True
4319
4366
  return False
4320
4367
 
4321
4368
 
@@ -4419,7 +4466,8 @@ def _maybe_inject_recon_convergence(
4419
4466
  observed failure mode of an agentic recon task wandering for hundreds
4420
4467
  of turns and never converging to the synthesis/write step. Two
4421
4468
  escalation tiers: a firm "switch to synthesis" directive, then a hard
4422
- "STOP, write it now" once the streak is 2x over threshold.
4469
+ "STOP, write it now" once the streak crosses PROXY_RECON_HARD_MULTIPLIER
4470
+ x threshold (default 1.5x).
4423
4471
 
4424
4472
  `full_tools` is the request's tool list *before* `_narrow_tools_for_request`
4425
4473
  pruned it. When the directive fires, any write/deliverable tool that
@@ -4436,7 +4484,7 @@ def _maybe_inject_recon_convergence(
4436
4484
  # Report the *raw* (pre-prune) utilization — post-prune util understates the
4437
4485
  # blow-up (~30%) and makes the directive's "context is at X%" misleading.
4438
4486
  util = max(monitor.get_utilization(), monitor.get_raw_utilization())
4439
- hard = streak >= 2 * PROXY_RECON_CONVERGENCE_THRESHOLD
4487
+ hard = streak >= PROXY_RECON_HARD_MULTIPLIER * PROXY_RECON_CONVERGENCE_THRESHOLD
4440
4488
  escalate = False
4441
4489
  if hard:
4442
4490
  monitor.recon_hard_fires += 1 # Fix E: monotonic, never reset
@@ -4512,13 +4560,16 @@ def _maybe_inject_recon_convergence(
4512
4560
  # writing it rebuilds the streak and re-fires — bounded, not permanent.
4513
4561
  monitor.consecutive_no_write_turns = 0
4514
4562
  else:
4515
- if hard:
4516
- # Fix C: at the hard tier, drop the structural requirement to call a
4517
- # tool. Earlier logic forced tool_choice='required' for the active
4518
- # agentic loop, which directly contradicts "produce your deliverable
4519
- # NOW / do not run anything else" the model is forbidden from
4520
- # terminating and must emit yet another tool call, so the streak
4521
- # climbs unbounded. Releasing to 'auto' lets it actually write/stop.
4563
+ # Fix C (hard) + Fix B (firm): drop the structural "must call a tool"
4564
+ # coercion at BOTH directive tiers. The state machine forces
4565
+ # tool_choice='required' during the active agentic loop; paired with a
4566
+ # "switch to synthesis / write your deliverable now" directive that is a
4567
+ # trap -- the model cannot terminate and, when it does not pick the
4568
+ # write tool, emits another read, so the streak climbs unbounded.
4569
+ # Releasing to 'auto' lets it write, call deliver, or stop. (Previously
4570
+ # only the hard tier released; the firm tier left the coercion on, which
4571
+ # is where the observed read-forever loop lived.)
4572
+ if openai_body.get("tool_choice") == "required" or hard:
4522
4573
  openai_body["tool_choice"] = "auto"
4523
4574
  openai_body.pop("grammar", None)
4524
4575
  # Re-inject any write/deliverable tool that narrowing dropped, so the
@@ -41,5 +41,69 @@ class ReconDirectiveTest(unittest.TestCase):
41
41
  self.assertNotIn("being BLOCKED by policy", d)
42
42
 
43
43
 
44
+
45
+ class ProactiveGateDetectionTest(unittest.TestCase):
46
+ """A: gate detected from the harness banner BEFORE any write is attempted.
47
+
48
+ A read-forever session never triggers the reactive (blocked tool_result)
49
+ path, so the deliver redirect must fire from the gate banner in context."""
50
+
51
+ def test_system_banner_route_through_deliver(self):
52
+ body = {"messages": [
53
+ {"role": "system", "content": "Writing code — route through deliver. Use the deliver tool."},
54
+ {"role": "user", "content": "read the next file"},
55
+ ]}
56
+ self.assertTrue(ap._writes_are_gated(body))
57
+
58
+ def test_gated_and_will_be_blocked_banner(self):
59
+ body = {"messages": [
60
+ {"role": "user", "content": "direct Edit/Write on source files is gated and will be blocked"},
61
+ ]}
62
+ self.assertTrue(ap._writes_are_gated(body))
63
+
64
+ def test_no_banner_and_no_block_is_ungated(self):
65
+ body = {"messages": [
66
+ {"role": "system", "content": "You are a helpful assistant."},
67
+ {"role": "user", "content": "read the next file"},
68
+ ]}
69
+ self.assertFalse(ap._writes_are_gated(body))
70
+
71
+
72
+ class DeliverIsWriteToolTest(unittest.TestCase):
73
+ """A: deliver is the only write path under a gate; it must count as a write."""
74
+
75
+ def test_deliver_in_write_tool_class(self):
76
+ self.assertIn("deliver", ap._WRITE_TOOL_CLASS)
77
+
78
+
79
+ class FirmTierReleasesToolChoiceTest(unittest.TestCase):
80
+ """B: the firm tier must release the 'required' coercion so the model can
81
+ actually write / call deliver / stop instead of being forced into a read."""
82
+
83
+ def test_firm_tier_releases_required(self):
84
+ m = ap.SessionMonitor(context_window=132096)
85
+ m.consecutive_no_write_turns = ap.PROXY_RECON_CONVERGENCE_THRESHOLD # firm
86
+ body = {"messages": [{"role": "user", "content": "reading"}],
87
+ "tools": [{"name": "Write"}], "tool_choice": "required"}
88
+ ap._maybe_inject_recon_convergence(body, m)
89
+ self.assertEqual(body.get("tool_choice"), "auto")
90
+
91
+
92
+ class HardMultiplierTest(unittest.TestCase):
93
+ """C: hard-tier onset is governed by the configurable multiplier (default
94
+ 1.5x, earlier than the old hard-coded 2x)."""
95
+
96
+ def test_multiplier_invariant(self):
97
+ self.assertGreaterEqual(ap.PROXY_RECON_HARD_MULTIPLIER, 1.0)
98
+
99
+ def test_hard_tier_directive_at_multiplier(self):
100
+ m = ap.SessionMonitor(context_window=132096)
101
+ streak = int(ap.PROXY_RECON_HARD_MULTIPLIER * ap.PROXY_RECON_CONVERGENCE_THRESHOLD) + 1
102
+ m.consecutive_no_write_turns = streak
103
+ body = {"messages": [{"role": "user", "content": "reading"}], "tools": [{"name": "Write"}]}
104
+ ap._maybe_inject_recon_convergence(body, m)
105
+ self.assertIn("STOP", body["messages"][-1]["content"])
106
+
107
+
44
108
  if __name__ == "__main__":
45
109
  unittest.main()