@miller-tech/uap 1.46.4 → 1.47.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/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +8 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +14 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +204 -11
- package/dist/cli/deliver.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +8 -0
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +12 -2
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/applier.d.ts +8 -0
- package/dist/delivery/applier.d.ts.map +1 -1
- package/dist/delivery/applier.js +35 -1
- package/dist/delivery/applier.js.map +1 -1
- package/dist/delivery/auto-optimizer.d.ts +11 -0
- package/dist/delivery/auto-optimizer.d.ts.map +1 -1
- package/dist/delivery/auto-optimizer.js +11 -2
- package/dist/delivery/auto-optimizer.js.map +1 -1
- package/dist/delivery/ci-watcher.d.ts +72 -0
- package/dist/delivery/ci-watcher.d.ts.map +1 -0
- package/dist/delivery/ci-watcher.js +221 -0
- package/dist/delivery/ci-watcher.js.map +1 -0
- package/dist/delivery/deploy-dev-gate.d.ts +44 -0
- package/dist/delivery/deploy-dev-gate.d.ts.map +1 -0
- package/dist/delivery/deploy-dev-gate.js +175 -0
- package/dist/delivery/deploy-dev-gate.js.map +1 -0
- package/dist/delivery/verifier-ladder.d.ts +77 -0
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +225 -3
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/docs/INDEX.md +2 -2
- package/docs/guides/AUTOMATIC.md +255 -61
- package/docs/guides/AUTOMATIC_FEATURES.md +164 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +201 -24
|
@@ -150,6 +150,15 @@ PROXY_LOOP_REPEAT_THRESHOLD = int(os.environ.get("PROXY_LOOP_REPEAT_THRESHOLD",
|
|
|
150
150
|
PROXY_CYCLE_TRIGGER_REPEAT = int(os.environ.get("PROXY_CYCLE_TRIGGER_REPEAT", "3"))
|
|
151
151
|
PROXY_FORCED_THRESHOLD = int(os.environ.get("PROXY_FORCED_THRESHOLD", "15"))
|
|
152
152
|
PROXY_NO_PROGRESS_THRESHOLD = int(os.environ.get("PROXY_NO_PROGRESS_THRESHOLD", "3"))
|
|
153
|
+
# Fix D: streak-independent escape hatch. `no_progress_streak` resets to 0 on
|
|
154
|
+
# every turn whose last user message carries a tool_result (line ~3835) — i.e.
|
|
155
|
+
# every turn of a normal agentic loop — so the no_progress-gated LOOP BREAKER
|
|
156
|
+
# patterns can never accumulate. After this many *consecutive* forced-'required'
|
|
157
|
+
# turns (which DOES accumulate across an agentic loop via consecutive_forced_count),
|
|
158
|
+
# release tool_choice to 'auto' regardless of no_progress_streak so the model can
|
|
159
|
+
# emit a terminating response. Set well above any healthy run length (healthy
|
|
160
|
+
# loops hit auto/finalize/review phases that reset the count). 0 disables.
|
|
161
|
+
PROXY_FORCED_HARD_RELEASE = int(os.environ.get("PROXY_FORCED_HARD_RELEASE", "30"))
|
|
153
162
|
PROXY_CONTEXT_RELEASE_THRESHOLD = float(
|
|
154
163
|
os.environ.get("PROXY_CONTEXT_RELEASE_THRESHOLD", "0.90")
|
|
155
164
|
)
|
|
@@ -258,6 +267,34 @@ PROXY_FINALIZE_SESSION_HARD_CAP = int(
|
|
|
258
267
|
PROXY_RECON_CONVERGENCE_THRESHOLD = int(
|
|
259
268
|
os.environ.get("PROXY_RECON_CONVERGENCE_THRESHOLD", "40")
|
|
260
269
|
)
|
|
270
|
+
# Fix E: the recon hard tier (streak >= 2x threshold) fires a directive + flips
|
|
271
|
+
# tool_choice to 'auto', but `consecutive_no_write_turns` resets to 0 whenever
|
|
272
|
+
# the model emits any write tool — so a loop that periodically writes sawtooths
|
|
273
|
+
# the streak (observed: 90 -> 0 -> climb again), re-triggering the hard tier
|
|
274
|
+
# forever and never actually terminating. This counts how many times the hard
|
|
275
|
+
# tier has fired across the whole session (monotonic, never reset). Once it
|
|
276
|
+
# reaches this cap, the guard escalates: it strips tools for the turn so the
|
|
277
|
+
# model is forced to emit a terminal prose summary, breaking the sawtooth. 0
|
|
278
|
+
# disables the escalation (hard tier still flips to 'auto' each time).
|
|
279
|
+
PROXY_RECON_SESSION_HARD_CAP = int(
|
|
280
|
+
os.environ.get("PROXY_RECON_SESSION_HARD_CAP", "3")
|
|
281
|
+
)
|
|
282
|
+
# Fix F: context death-spiral breaker. When the *raw* (pre-prune) incoming
|
|
283
|
+
# context stays catastrophically over the window for several consecutive turns,
|
|
284
|
+
# releasing tool_choice to 'auto' (Fix B / LOOP BREAKER) is NOT enough — the
|
|
285
|
+
# model keeps voluntarily emitting tool calls and the client keeps resending an
|
|
286
|
+
# ever-growing transcript (observed: ctx 936%, model emits tool_calls 18/min
|
|
287
|
+
# despite tool_choice=auto). After this many consecutive turns at/above the
|
|
288
|
+
# ratio, strip tools entirely so the only possible response is a terminal text
|
|
289
|
+
# summary (end_turn), which ends the client's agentic loop. Ratio is set high
|
|
290
|
+
# enough that only a true runaway trips it — a merely-full session tops out near
|
|
291
|
+
# 100-130%, never 300%. 0 disables.
|
|
292
|
+
PROXY_RAW_CTX_FINALIZE_RATIO = float(
|
|
293
|
+
os.environ.get("PROXY_RAW_CTX_FINALIZE_RATIO", "3.0")
|
|
294
|
+
)
|
|
295
|
+
PROXY_RAW_CTX_FINALIZE_STREAK = int(
|
|
296
|
+
os.environ.get("PROXY_RAW_CTX_FINALIZE_STREAK", "2")
|
|
297
|
+
)
|
|
261
298
|
PROXY_STREAM_REASONING_FALLBACK = (
|
|
262
299
|
os.environ.get("PROXY_STREAM_REASONING_FALLBACK", "off").strip().lower()
|
|
263
300
|
)
|
|
@@ -800,6 +837,13 @@ class SessionMonitor:
|
|
|
800
837
|
last_input_tokens: int = 0 # Estimated input tokens of last request
|
|
801
838
|
last_output_tokens: int = 0 # Actual output tokens of last response
|
|
802
839
|
peak_input_tokens: int = 0 # High-water mark
|
|
840
|
+
# Fix B: the incoming (pre-prune) token count for the current request. The
|
|
841
|
+
# proxy prunes the conversation and then calls record_request() again with
|
|
842
|
+
# the post-prune total, so last_input_tokens / get_utilization() reflect the
|
|
843
|
+
# *pruned* size (~30%) by the time the tool_choice guards run — masking the
|
|
844
|
+
# fact that the client just sent e.g. 800% of the window. This preserves the
|
|
845
|
+
# raw size so LOOP BREAKER pattern 3 can release on real context blow-up.
|
|
846
|
+
pre_prune_input_tokens: int = 0
|
|
803
847
|
prune_count: int = 0 # How many times pruning was triggered
|
|
804
848
|
overflow_count: int = 0 # How many context overflow errors caught
|
|
805
849
|
prune_drop_count: int = 0 # monotonic: # of oldest middle msgs pruned (B3)
|
|
@@ -818,6 +862,8 @@ class SessionMonitor:
|
|
|
818
862
|
loop_warnings_emitted: int = 0 # How many loop warnings sent to the model
|
|
819
863
|
no_progress_streak: int = 0 # Forced tool turns without new tool_result
|
|
820
864
|
consecutive_no_write_turns: int = 0 # turns exploring with no write tool (B1)
|
|
865
|
+
recon_hard_fires: int = 0 # Fix E: monotonic count of recon hard-tier firings
|
|
866
|
+
catastrophic_ctx_streak: int = 0 # Fix F: consecutive turns raw ctx >= finalize ratio
|
|
821
867
|
unexpected_end_turn_count: int = 0 # end_turn without tool_use in active loop
|
|
822
868
|
tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
|
|
823
869
|
malformed_tool_streak: int = 0 # consecutive malformed pseudo tool payloads
|
|
@@ -878,6 +924,18 @@ class SessionMonitor:
|
|
|
878
924
|
return 0.0
|
|
879
925
|
return self.last_input_tokens / self.context_window
|
|
880
926
|
|
|
927
|
+
def get_raw_utilization(self) -> float:
|
|
928
|
+
"""Pre-prune context utilization for the current request (Fix B).
|
|
929
|
+
|
|
930
|
+
Reflects what the client actually sent this turn, before the proxy
|
|
931
|
+
pruned it. Used by the loop breaker so a runaway client that resends
|
|
932
|
+
800% of the window each turn is detected even though post-prune
|
|
933
|
+
utilization reads ~30%. Returns 0.0 until the first request is recorded.
|
|
934
|
+
"""
|
|
935
|
+
if self.context_window <= 0:
|
|
936
|
+
return 0.0
|
|
937
|
+
return self.pre_prune_input_tokens / self.context_window
|
|
938
|
+
|
|
881
939
|
def get_warning_level(self) -> str | None:
|
|
882
940
|
"""Return warning level based on context utilization.
|
|
883
941
|
Returns None if no warning needed."""
|
|
@@ -1238,12 +1296,37 @@ class SessionMonitor:
|
|
|
1238
1296
|
self.loop_warnings_emitted += 1
|
|
1239
1297
|
return True
|
|
1240
1298
|
|
|
1241
|
-
# Pattern
|
|
1242
|
-
|
|
1299
|
+
# Pattern 2b (Fix D): streak-independent forced-count ceiling. In an
|
|
1300
|
+
# agentic loop no_progress_streak resets every turn (tool_result always
|
|
1301
|
+
# present), so Pattern 2 never fires. consecutive_forced_count, however,
|
|
1302
|
+
# accumulates across the loop. Release once it crosses the hard ceiling
|
|
1303
|
+
# regardless of no_progress_streak so the model can terminate.
|
|
1304
|
+
if (
|
|
1305
|
+
PROXY_FORCED_HARD_RELEASE > 0
|
|
1306
|
+
and self.consecutive_forced_count >= PROXY_FORCED_HARD_RELEASE
|
|
1307
|
+
):
|
|
1308
|
+
logger.warning(
|
|
1309
|
+
"LOOP BREAKER: %d consecutive forced tool_choice requests (hard ceiling %d) -- "
|
|
1310
|
+
"releasing to 'auto' regardless of progress streak.",
|
|
1311
|
+
self.consecutive_forced_count,
|
|
1312
|
+
PROXY_FORCED_HARD_RELEASE,
|
|
1313
|
+
)
|
|
1314
|
+
self.loop_warnings_emitted += 1
|
|
1315
|
+
return True
|
|
1316
|
+
|
|
1317
|
+
# Pattern 3: Context almost full -- let model wrap up naturally.
|
|
1318
|
+
# Fix B: check BOTH post-prune utilization and the raw pre-prune size.
|
|
1319
|
+
# The proxy prunes before this runs, so get_utilization() reads ~30%
|
|
1320
|
+
# even when the client just sent 800% of the window; get_raw_utilization()
|
|
1321
|
+
# exposes the real blow-up so a runaway client is actually released.
|
|
1322
|
+
eff_util = max(self.get_utilization(), self.get_raw_utilization())
|
|
1323
|
+
if eff_util >= PROXY_CONTEXT_RELEASE_THRESHOLD:
|
|
1243
1324
|
logger.warning(
|
|
1244
|
-
"LOOP BREAKER: Context utilization %.1f%% --
|
|
1245
|
-
"tool_choice to let model wrap up.",
|
|
1325
|
+
"LOOP BREAKER: Context utilization %.1f%% (post-prune %.1f%%, raw %.1f%%) -- "
|
|
1326
|
+
"releasing tool_choice to let model wrap up.",
|
|
1327
|
+
eff_util * 100,
|
|
1246
1328
|
self.get_utilization() * 100,
|
|
1329
|
+
self.get_raw_utilization() * 100,
|
|
1247
1330
|
)
|
|
1248
1331
|
return True
|
|
1249
1332
|
|
|
@@ -3397,8 +3480,32 @@ def _maybe_inject_recon_convergence(
|
|
|
3397
3480
|
streak = monitor.consecutive_no_write_turns
|
|
3398
3481
|
if streak < PROXY_RECON_CONVERGENCE_THRESHOLD:
|
|
3399
3482
|
return
|
|
3400
|
-
|
|
3401
|
-
|
|
3483
|
+
# Report the *raw* (pre-prune) utilization — post-prune util understates the
|
|
3484
|
+
# blow-up (~30%) and makes the directive's "context is at X%" misleading.
|
|
3485
|
+
util = max(monitor.get_utilization(), monitor.get_raw_utilization())
|
|
3486
|
+
hard = streak >= 2 * PROXY_RECON_CONVERGENCE_THRESHOLD
|
|
3487
|
+
escalate = False
|
|
3488
|
+
if hard:
|
|
3489
|
+
monitor.recon_hard_fires += 1 # Fix E: monotonic, never reset
|
|
3490
|
+
escalate = (
|
|
3491
|
+
PROXY_RECON_SESSION_HARD_CAP > 0
|
|
3492
|
+
and monitor.recon_hard_fires >= PROXY_RECON_SESSION_HARD_CAP
|
|
3493
|
+
)
|
|
3494
|
+
|
|
3495
|
+
if escalate:
|
|
3496
|
+
# Fix E: the hard tier has fired repeatedly this session — the model
|
|
3497
|
+
# keeps writing just enough to reset consecutive_no_write_turns, then
|
|
3498
|
+
# re-diverges, sawtoothing the streak and re-triggering the hard tier
|
|
3499
|
+
# forever. Stop negotiating: strip tools so the model MUST emit a
|
|
3500
|
+
# terminal plain-text summary, breaking the sawtooth for good.
|
|
3501
|
+
directive = (
|
|
3502
|
+
f"STOP. You have hit the exploration limit {monitor.recon_hard_fires} "
|
|
3503
|
+
f"times in this session and context is at {util * 100:.0f}%. No tools "
|
|
3504
|
+
"are available this turn. Reply NOW with a plain-text summary of what "
|
|
3505
|
+
"you found and what remains — this ends the task."
|
|
3506
|
+
)
|
|
3507
|
+
tier = "hard-escalated"
|
|
3508
|
+
elif hard:
|
|
3402
3509
|
directive = (
|
|
3403
3510
|
f"STOP exploring. You have run {streak} consecutive turns of "
|
|
3404
3511
|
f"exploration without producing a deliverable and context is at "
|
|
@@ -3421,27 +3528,43 @@ def _maybe_inject_recon_convergence(
|
|
|
3421
3528
|
msgs.append({"role": "user", "content": directive})
|
|
3422
3529
|
openai_body["messages"] = msgs
|
|
3423
3530
|
|
|
3424
|
-
# Re-inject any write/deliverable tool that narrowing dropped, so the
|
|
3425
|
-
# "write your deliverable" directive is actually satisfiable. Without
|
|
3426
|
-
# this the model is told to write but has no write tool to call, picks
|
|
3427
|
-
# another read tool, and the streak climbs unbounded.
|
|
3428
3531
|
restored: list[str] = []
|
|
3429
|
-
if
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3532
|
+
if escalate:
|
|
3533
|
+
# Strip tools entirely so the only possible response is terminal prose.
|
|
3534
|
+
openai_body.pop("tools", None)
|
|
3535
|
+
openai_body.pop("tool_choice", None)
|
|
3536
|
+
openai_body.pop("grammar", None)
|
|
3537
|
+
else:
|
|
3538
|
+
if hard:
|
|
3539
|
+
# Fix C: at the hard tier, drop the structural requirement to call a
|
|
3540
|
+
# tool. Earlier logic forced tool_choice='required' for the active
|
|
3541
|
+
# agentic loop, which directly contradicts "produce your deliverable
|
|
3542
|
+
# NOW / do not run anything else" — the model is forbidden from
|
|
3543
|
+
# terminating and must emit yet another tool call, so the streak
|
|
3544
|
+
# climbs unbounded. Releasing to 'auto' lets it actually write/stop.
|
|
3545
|
+
openai_body["tool_choice"] = "auto"
|
|
3546
|
+
openai_body.pop("grammar", None)
|
|
3547
|
+
# Re-inject any write/deliverable tool that narrowing dropped, so the
|
|
3548
|
+
# "write your deliverable" directive is actually satisfiable. Without
|
|
3549
|
+
# this the model is told to write but has no write tool to call, picks
|
|
3550
|
+
# another read tool, and the streak climbs unbounded.
|
|
3551
|
+
if full_tools:
|
|
3552
|
+
present = {
|
|
3553
|
+
(t.get("function", {}).get("name", "") or "").lower()
|
|
3554
|
+
for t in openai_body.get("tools", [])
|
|
3555
|
+
}
|
|
3556
|
+
for tool in full_tools:
|
|
3557
|
+
name = (tool.get("function", {}).get("name", "") or "")
|
|
3558
|
+
if name.lower() in _WRITE_TOOL_CLASS and name.lower() not in present:
|
|
3559
|
+
openai_body.setdefault("tools", []).append(tool)
|
|
3560
|
+
present.add(name.lower())
|
|
3561
|
+
restored.append(name)
|
|
3440
3562
|
|
|
3441
3563
|
logger.warning(
|
|
3442
|
-
"RECON CONVERGENCE: injected %s directive (no_write_streak=%d,
|
|
3443
|
-
"restored_write_tools=%s)",
|
|
3444
|
-
tier, streak, util * 100,
|
|
3564
|
+
"RECON CONVERGENCE: injected %s directive (no_write_streak=%d, hard_fires=%d, "
|
|
3565
|
+
"ctx=%.0f%%, tool_choice=%s, restored_write_tools=%s)",
|
|
3566
|
+
tier, streak, monitor.recon_hard_fires, util * 100,
|
|
3567
|
+
openai_body.get("tool_choice", "stripped"), restored or "none",
|
|
3445
3568
|
)
|
|
3446
3569
|
|
|
3447
3570
|
|
|
@@ -3751,6 +3874,46 @@ def build_openai_request(
|
|
|
3751
3874
|
last_user_has_tool_result,
|
|
3752
3875
|
)
|
|
3753
3876
|
|
|
3877
|
+
# CONTEXT DEATH-SPIRAL BREAKER (Fix F): raw incoming context has been
|
|
3878
|
+
# catastrophically over the window for several consecutive turns. The
|
|
3879
|
+
# LOOP BREAKER already released tool_choice to 'auto', but the model
|
|
3880
|
+
# keeps voluntarily emitting tool calls and the client keeps resending a
|
|
3881
|
+
# growing transcript, so the loop never ends. Strip tools entirely so
|
|
3882
|
+
# the only possible output is a terminal text summary (end_turn), which
|
|
3883
|
+
# ends the client's agentic loop. Gated high (raw ctx >= 300% for >= N
|
|
3884
|
+
# turns) so only a true runaway trips it, never a merely-full session.
|
|
3885
|
+
if (
|
|
3886
|
+
PROXY_RAW_CTX_FINALIZE_STREAK > 0
|
|
3887
|
+
and monitor.catastrophic_ctx_streak >= PROXY_RAW_CTX_FINALIZE_STREAK
|
|
3888
|
+
):
|
|
3889
|
+
openai_body.pop("tool_choice", None)
|
|
3890
|
+
openai_body.pop("tools", None)
|
|
3891
|
+
openai_body.pop("grammar", None)
|
|
3892
|
+
msgs = openai_body.get("messages", [])
|
|
3893
|
+
msgs.append({
|
|
3894
|
+
"role": "user",
|
|
3895
|
+
"content": (
|
|
3896
|
+
"The conversation has exceeded the context window "
|
|
3897
|
+
f"({monitor.get_raw_utilization() * 100:.0f}%) and cannot "
|
|
3898
|
+
"continue. No tools are available. Reply with a brief "
|
|
3899
|
+
"plain-text summary of what was accomplished and what "
|
|
3900
|
+
"remains, then stop."
|
|
3901
|
+
),
|
|
3902
|
+
})
|
|
3903
|
+
openai_body["messages"] = msgs
|
|
3904
|
+
monitor.reset_tool_turn_state(reason="context_death_spiral_breaker")
|
|
3905
|
+
logger.error(
|
|
3906
|
+
"CONTEXT DEATH-SPIRAL BREAKER: raw ctx %.0f%% for %d consecutive "
|
|
3907
|
+
"turns -- stripped tools to force terminal summary (end_turn).",
|
|
3908
|
+
monitor.get_raw_utilization() * 100,
|
|
3909
|
+
monitor.catastrophic_ctx_streak,
|
|
3910
|
+
)
|
|
3911
|
+
if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
|
|
3912
|
+
openai_body["enable_thinking"] = False
|
|
3913
|
+
if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
|
|
3914
|
+
openai_body["speculative.n_max"] = 0
|
|
3915
|
+
return openai_body
|
|
3916
|
+
|
|
3754
3917
|
# TOOL STARVATION BREAKER: if model repeatedly fails to produce tool
|
|
3755
3918
|
# calls despite required, strip tools to let it generate text and break
|
|
3756
3919
|
# the forcing loop.
|
|
@@ -7834,6 +7997,20 @@ async def messages(request: Request):
|
|
|
7834
7997
|
estimated_tokens,
|
|
7835
7998
|
)
|
|
7836
7999
|
utilization = effective_tokens / ctx_window
|
|
8000
|
+
# Fix B: preserve the raw incoming size before any pruning rewrites
|
|
8001
|
+
# last_input_tokens to the post-prune total, so the loop breaker can
|
|
8002
|
+
# see the true blow-up at build_openai_request time.
|
|
8003
|
+
monitor.pre_prune_input_tokens = effective_tokens
|
|
8004
|
+
# Fix F: track consecutive turns whose raw incoming context is
|
|
8005
|
+
# catastrophically over the window (a death spiral the per-request
|
|
8006
|
+
# pruner can mask but not cure). build_openai_request acts on this.
|
|
8007
|
+
if (
|
|
8008
|
+
PROXY_RAW_CTX_FINALIZE_RATIO > 0
|
|
8009
|
+
and utilization >= PROXY_RAW_CTX_FINALIZE_RATIO
|
|
8010
|
+
):
|
|
8011
|
+
monitor.catastrophic_ctx_streak += 1
|
|
8012
|
+
else:
|
|
8013
|
+
monitor.catastrophic_ctx_streak = 0
|
|
7837
8014
|
if utilization >= PROXY_CONTEXT_PRUNE_THRESHOLD:
|
|
7838
8015
|
logger.warning(
|
|
7839
8016
|
"Context utilization %.1f%% exceeds threshold %.1f%% -- pruning conversation",
|