@miller-tech/uap 1.133.1 → 1.134.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.
Files changed (40) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +4 -1
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/config-command.d.ts +49 -0
  5. package/dist/cli/config-command.d.ts.map +1 -0
  6. package/dist/cli/config-command.js +465 -0
  7. package/dist/cli/config-command.js.map +1 -0
  8. package/dist/cli/config-wizard.d.ts +13 -0
  9. package/dist/cli/config-wizard.d.ts.map +1 -0
  10. package/dist/cli/config-wizard.js +134 -0
  11. package/dist/cli/config-wizard.js.map +1 -0
  12. package/dist/cli/guided-setup.d.ts.map +1 -1
  13. package/dist/cli/guided-setup.js +11 -0
  14. package/dist/cli/guided-setup.js.map +1 -1
  15. package/dist/cli/policy.d.ts.map +1 -1
  16. package/dist/cli/policy.js +34 -0
  17. package/dist/cli/policy.js.map +1 -1
  18. package/dist/cli/setup.d.ts +1 -1
  19. package/dist/cli/setup.d.ts.map +1 -1
  20. package/dist/cli/setup.js +22 -0
  21. package/dist/cli/setup.js.map +1 -1
  22. package/dist/config/policy-recommendations.d.ts +29 -0
  23. package/dist/config/policy-recommendations.d.ts.map +1 -0
  24. package/dist/config/policy-recommendations.js +108 -0
  25. package/dist/config/policy-recommendations.js.map +1 -0
  26. package/dist/config/settings-registry.d.ts +65 -0
  27. package/dist/config/settings-registry.d.ts.map +1 -0
  28. package/dist/config/settings-registry.js +334 -0
  29. package/dist/config/settings-registry.js.map +1 -0
  30. package/dist/dashboard/server.d.ts +9 -0
  31. package/dist/dashboard/server.d.ts.map +1 -1
  32. package/dist/dashboard/server.js +21 -6
  33. package/dist/dashboard/server.js.map +1 -1
  34. package/docs/INDEX.md +2 -0
  35. package/docs/guides/POLICY_SELECTION.md +98 -0
  36. package/docs/reference/CONFIGURATION_REFERENCE.md +688 -0
  37. package/package.json +1 -1
  38. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  39. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  40. package/tools/agents/scripts/anthropic_proxy.py +137 -12
@@ -184,6 +184,27 @@ PROXY_CONTEXT_WINDOW = int(os.environ.get("PROXY_CONTEXT_WINDOW", "0"))
184
184
  PROXY_CONTEXT_PRUNE_THRESHOLD = float(
185
185
  os.environ.get("PROXY_CONTEXT_PRUNE_THRESHOLD", "0.85")
186
186
  )
187
+ # Compaction forcing (Option A, 2026-07-10): Claude Code decides when to
188
+ # auto-compact against ITS believed model window (~200k) using
189
+ # /v1/messages/count_tokens, so an HONEST count on a smaller local rail lets
190
+ # every session grow into the "thrash band" — above the rail (per-request
191
+ # critical prunes, findings evaporate, read-only doom loops; observed live:
192
+ # 87 reads / 0 writes in 30 min at 122% utilization) but below the client's
193
+ # compact trigger (~92.5% of 200k). Scaling the reported count makes the
194
+ # client compact BEFORE the rail. "auto" (default) derives the scale from the
195
+ # LIVE rail at call time — assumed_window / (rail * target_fraction) — so it
196
+ # tracks rail resizes; a number forces that scale; "1"/"off" disables.
197
+ PROXY_COUNT_TOKENS_SCALE = os.environ.get("PROXY_COUNT_TOKENS_SCALE", "auto")
198
+ PROXY_CLIENT_ASSUMED_WINDOW = int(
199
+ os.environ.get("PROXY_CLIENT_ASSUMED_WINDOW", "200000")
200
+ )
201
+ # Where compaction should land sessions, as a fraction of the rail. Unset
202
+ # (0) = auto: just under the pruner threshold (x0.95), so the client's own
203
+ # compaction fires BEFORE the proxy ever needs to prune — the pruner stays on
204
+ # purely as a backstop. An explicit value overrides (clamped to <1).
205
+ PROXY_COMPACT_TARGET_FRACTION = float(
206
+ os.environ.get("PROXY_COMPACT_TARGET_FRACTION", "0")
207
+ )
187
208
  PROXY_CONTEXT_PRUNE_TARGET_FRACTION = float(
188
209
  os.environ.get("PROXY_CONTEXT_PRUNE_TARGET_FRACTION", "0.50")
189
210
  )
@@ -8936,7 +8957,7 @@ async def _maybe_apply_recipe(anthropic_resp, anthropic_body, openai_body, clien
8936
8957
  return anthropic_resp
8937
8958
 
8938
8959
 
8939
- async def _heartbeat_then_buffered(produce_coro, model: str):
8960
+ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int = 0):
8940
8961
  """SSE generator: keep-alive heartbeat wrapper for the guarded-non-stream path.
8941
8962
 
8942
8963
  Emits an immediate ``message_start`` so the client registers an active
@@ -8953,7 +8974,7 @@ async def _heartbeat_then_buffered(produce_coro, model: str):
8953
8974
  msg_id = f"msg_{uuid.uuid4().hex[:24]}"
8954
8975
  yield (
8955
8976
  f"event: message_start\n"
8956
- f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"
8977
+ f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': input_tokens, 'output_tokens': 0}}})}\n\n"
8957
8978
  )
8958
8979
  interval = PROXY_STREAM_HEARTBEAT_SECS if PROXY_STREAM_HEARTBEAT_SECS > 0 else 15.0
8959
8980
  task = asyncio.ensure_future(produce_coro)
@@ -9011,7 +9032,13 @@ async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: boo
9011
9032
  "model": anthropic_resp.get("model", "unknown"),
9012
9033
  "stop_reason": None,
9013
9034
  "stop_sequence": None,
9014
- "usage": {"input_tokens": 0, "output_tokens": 0},
9035
+ "usage": {
9036
+ "input_tokens": int(
9037
+ (anthropic_resp.get("usage", {}) or {}).get("input_tokens", 0)
9038
+ * _count_tokens_scale()
9039
+ ),
9040
+ "output_tokens": 0,
9041
+ },
9015
9042
  }
9016
9043
  yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': message})}\n\n"
9017
9044
 
@@ -9053,11 +9080,13 @@ async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: boo
9053
9080
  )
9054
9081
  block_index += 1
9055
9082
 
9056
- output_tokens = anthropic_resp.get("usage", {}).get("output_tokens", 0)
9083
+ resp_usage = anthropic_resp.get("usage", {}) or {}
9084
+ output_tokens = resp_usage.get("output_tokens", 0)
9085
+ scaled_input = int(resp_usage.get("input_tokens", 0) * _count_tokens_scale())
9057
9086
  stop_reason = anthropic_resp.get("stop_reason", "end_turn")
9058
9087
  yield (
9059
9088
  "event: message_delta\n"
9060
- f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}})}\n\n"
9089
+ f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': {'input_tokens': scaled_input, 'output_tokens': output_tokens}})}\n\n"
9061
9090
  )
9062
9091
  yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
9063
9092
 
@@ -9084,10 +9113,11 @@ async def stream_anthropic_response(
9084
9113
  msg_id = f"msg_{uuid.uuid4().hex[:24]}"
9085
9114
  stream_started = time.monotonic()
9086
9115
 
9087
- # message_start
9116
+ # message_start — carries the client's (scaled) input size; a hardcoded 0
9117
+ # here blinded clients to their own context usage (compaction never fired).
9088
9118
  yield (
9089
9119
  f"event: message_start\n"
9090
- f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"
9120
+ f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': _client_input_tokens(monitor), 'output_tokens': 0}}})}\n\n"
9091
9121
  )
9092
9122
 
9093
9123
  # content_block_start for text (index 0)
@@ -9392,10 +9422,12 @@ async def stream_anthropic_response(
9392
9422
  if _is_unexpected_end_turn(synthetic_openai_resp, anthropic_body):
9393
9423
  monitor.unexpected_end_turn_count += 1
9394
9424
 
9395
- # message_delta with final stop reason
9425
+ # message_delta with final stop reason. Repeats the (scaled) input size so
9426
+ # clients that read usage from the delta rather than message_start still
9427
+ # see their context usage — the compaction-forcing signal.
9396
9428
  yield (
9397
9429
  f"event: message_delta\n"
9398
- f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': finish_reason, 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}})}\n\n"
9430
+ f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': finish_reason, 'stop_sequence': None}, 'usage': {'input_tokens': _client_input_tokens(monitor), 'output_tokens': output_tokens}})}\n\n"
9399
9431
  )
9400
9432
 
9401
9433
  # message_stop
@@ -9563,7 +9595,98 @@ async def count_tokens(request: Request):
9563
9595
  status_code=400,
9564
9596
  media_type="application/json",
9565
9597
  )
9566
- return {"input_tokens": estimate_total_tokens(body)}
9598
+ est = estimate_total_tokens(body)
9599
+ scale = _count_tokens_scale()
9600
+ if scale > 1.0:
9601
+ scaled = int(est * scale)
9602
+ # Once per scale value, explain the discrepancy an operator would
9603
+ # otherwise chase between this endpoint and the pruner's numbers.
9604
+ global _count_scale_logged
9605
+ if _count_scale_logged != scale:
9606
+ _count_scale_logged = scale
9607
+ logger.info(
9608
+ "COMPACT FORCING: scaling count_tokens by %.2f "
9609
+ "(client assumed window %d, rail %d) — client auto-compact "
9610
+ "fires at ~%d real tokens, before the pruner",
9611
+ scale,
9612
+ PROXY_CLIENT_ASSUMED_WINDOW,
9613
+ default_context_window if default_context_window > 0 else PROXY_CONTEXT_WINDOW,
9614
+ int(PROXY_CLIENT_ASSUMED_WINDOW * 0.925 / scale),
9615
+ )
9616
+ return {"input_tokens": scaled}
9617
+ return {"input_tokens": est}
9618
+
9619
+
9620
+ _count_scale_logged: float = 0.0
9621
+
9622
+
9623
+ def _count_tokens_scale() -> float:
9624
+ """Resolve the count_tokens compaction-forcing scale (>= 1.0; 1.0 = off).
9625
+
9626
+ "auto" derives it from the LIVE rail each call so a rail resize (server
9627
+ restart with a different --ctx-size) re-tunes the client's compact point
9628
+ without a proxy restart.
9629
+ """
9630
+ raw = PROXY_COUNT_TOKENS_SCALE.strip().lower()
9631
+ if raw in ("", "off", "none", "0", "1", "1.0"):
9632
+ return 1.0
9633
+ if raw != "auto":
9634
+ try:
9635
+ return max(1.0, float(raw))
9636
+ except ValueError:
9637
+ return 1.0
9638
+ window = default_context_window if default_context_window > 0 else PROXY_CONTEXT_WINDOW
9639
+ if window <= 0:
9640
+ return 1.0
9641
+ frac = (
9642
+ PROXY_COMPACT_TARGET_FRACTION
9643
+ if 0 < PROXY_COMPACT_TARGET_FRACTION < 1
9644
+ # Auto: land compaction just under the pruner trigger so the client
9645
+ # compacts before the proxy ever prunes (pruner = backstop only).
9646
+ else min(0.9, PROXY_CONTEXT_PRUNE_THRESHOLD * 0.95)
9647
+ )
9648
+ target = window * frac
9649
+ if target <= 0 or PROXY_CLIENT_ASSUMED_WINDOW <= target:
9650
+ # Rail already exceeds the client's own compact point — honest counts
9651
+ # are fine, the client compacts before the rail unaided.
9652
+ return 1.0
9653
+ return PROXY_CLIENT_ASSUMED_WINDOW / target
9654
+
9655
+
9656
+ def _client_input_tokens(monitor) -> int:
9657
+ """Input size to REPORT to the client, scaled for compaction forcing.
9658
+
9659
+ Streaming responses historically hardcoded ``input_tokens: 0`` in
9660
+ message_start — the client could never see its own context usage, so its
9661
+ auto-compaction NEVER fired and sessions ground against the pruner forever
9662
+ (the actual root cause of the interactive thrash band; count_tokens
9663
+ scaling alone was insufficient because the dominant clients don't call
9664
+ it). Reports the PRE-prune estimate — the client's own conversation size,
9665
+ which is what its compaction decision is about — times the forcing scale.
9666
+ """
9667
+ est = (
9668
+ getattr(monitor, "pre_prune_input_tokens", 0)
9669
+ or getattr(monitor, "last_input_tokens", 0)
9670
+ or 0
9671
+ )
9672
+ return int(est * _count_tokens_scale())
9673
+
9674
+
9675
+ def _scale_client_usage(anthropic_resp: dict) -> dict:
9676
+ """Shallow-copy a finalized response with usage.input_tokens scaled for the
9677
+ CLIENT. Internal consumers (pruner accounting, telemetry) read the original
9678
+ dict's honest numbers — only the wire copy lies, and it lies consistently
9679
+ with count_tokens and the streaming frames."""
9680
+ scale = _count_tokens_scale()
9681
+ usage = anthropic_resp.get("usage")
9682
+ if scale <= 1.0 or not isinstance(usage, dict):
9683
+ return anthropic_resp
9684
+ out = dict(anthropic_resp)
9685
+ out["usage"] = {
9686
+ **usage,
9687
+ "input_tokens": int(usage.get("input_tokens", 0) * scale),
9688
+ }
9689
+ return out
9567
9690
 
9568
9691
 
9569
9692
  @app.post("/v1/messages")
@@ -10037,7 +10160,7 @@ async def messages(request: Request):
10037
10160
 
10038
10161
  if PROXY_STREAM_HEARTBEAT_SECS > 0:
10039
10162
  return StreamingResponse(
10040
- _heartbeat_then_buffered(_produce_guarded(), model),
10163
+ _heartbeat_then_buffered(_produce_guarded(), model, _client_input_tokens(monitor)),
10041
10164
  media_type="text/event-stream",
10042
10165
  headers={
10043
10166
  "Cache-Control": "no-cache",
@@ -10466,7 +10589,9 @@ async def messages(request: Request):
10466
10589
  await asyncio.to_thread(
10467
10590
  _record_project_telemetry, body, model, anthropic_resp.get("usage", {})
10468
10591
  )
10469
- return anthropic_resp
10592
+ # Wire copy only: telemetry above and monitor accounting keep the
10593
+ # honest numbers; the client sees the compaction-forcing scale.
10594
+ return _scale_client_usage(anthropic_resp)
10470
10595
 
10471
10596
 
10472
10597
  @app.post("/anthropic/v1/messages")