crprotocol 2.0.0__py3-none-any.whl

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 (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,53 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Resource management — allocation, monitoring, model registry, cost model."""
4
+
5
+ from crp.resources.adaptive_allocator import (
6
+ AdaptiveAllocator,
7
+ EnvelopeProfile,
8
+ ExtractionProfile,
9
+ PromptEfficiency,
10
+ WindowOverheadRecord,
11
+ detect_hardware,
12
+ )
13
+ from crp.resources.cost_model import (
14
+ KNOWN_PRICING,
15
+ BudgetWarning,
16
+ BudgetWarningLevel,
17
+ CostModel,
18
+ OverheadBudget,
19
+ OverheadDecision,
20
+ ProviderPricing,
21
+ WindowCost,
22
+ )
23
+ from crp.resources.overhead_manager import (
24
+ PROTECTED_INTELLIGENCE,
25
+ SHEDDING_CASCADE,
26
+ OverheadBudgetManager,
27
+ )
28
+ from crp.resources.resource_manager import (
29
+ MODEL_ESTIMATES,
30
+ ResourceManager,
31
+ ResourceSnapshot,
32
+ )
33
+
34
+ __all__ = [
35
+ "AdaptiveAllocator",
36
+ "CostModel",
37
+ "EnvelopeProfile",
38
+ "ExtractionProfile",
39
+ "MODEL_ESTIMATES",
40
+ "OverheadBudget",
41
+ "OverheadDecision",
42
+ "OverheadBudgetManager",
43
+ "ProviderPricing",
44
+ "ResourceManager",
45
+ "ResourceSnapshot",
46
+ "WindowCost",
47
+ "WindowOverheadRecord",
48
+ "BudgetWarning",
49
+ "BudgetWarningLevel",
50
+ "KNOWN_PRICING",
51
+ "SHEDDING_CASCADE",
52
+ "detect_hardware",
53
+ ]
@@ -0,0 +1,525 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Adaptive resource allocator — efficient CRP pipeline without cutting intelligence.
4
+
5
+ Integrates ResourceManager (memory pressure) with OverheadBudgetManager
6
+ (feature shedding) to dynamically adapt CRP's processing pipeline based
7
+ on real-time performance measurements.
8
+
9
+ Design philosophy:
10
+ CRP is an EFFICIENCY protocol, not a speed protocol. ML extraction
11
+ stages (GLiNER, UIE, Discourse) are core intelligence and are NEVER
12
+ disabled. Under pressure, the system *throttles throughput* (fewer
13
+ facts per stage, larger batches, prompt caching) rather than cutting
14
+ analytical capability.
15
+
16
+ Design goals:
17
+ 1. Keep CRP overhead below a configurable cap (default 15% of wall time)
18
+ 2. Shed optional optimization features under pressure (community detection,
19
+ cross-encoder reranking) while protecting ML intelligence
20
+ 3. Throttle throughput gracefully: reduce fact counts, increase batches
21
+ 4. Provide prompt-level efficiency hints (caching, dedup, connection reuse)
22
+ 5. Track per-window overhead history and adapt thresholds
23
+ 6. Provide hardware-aware defaults (CPU count, available RAM)
24
+
25
+ Efficiency strategies (applied in order of priority):
26
+ Phase 1 — Prompt efficiency (caching, deduplication, connection reuse)
27
+ Phase 2 — Feature shedding (community_detection, cross_encoder only)
28
+ Phase 3 — Throughput throttling (reduce facts per stage, larger batches)
29
+ Phase 4 — Model lifecycle (idle models freed after timeout)
30
+ Phase 5 — GC sweep (Python garbage collection under memory pressure)
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ import os
37
+ import time
38
+ import threading
39
+ from dataclasses import dataclass, field
40
+
41
+ from crp.resources.overhead_manager import OverheadBudgetManager, SHEDDING_CASCADE, PROTECTED_INTELLIGENCE
42
+ from crp.resources.resource_manager import ResourceManager
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ # ═══════════════════════════════════════════════════════════════════════════
48
+ # Hardware detection
49
+ # ═══════════════════════════════════════════════════════════════════════════
50
+
51
+ def detect_hardware() -> dict[str, int | float]:
52
+ """Detect available hardware resources (best-effort)."""
53
+ cpu_count = os.cpu_count() or 1
54
+ try:
55
+ if os.name == "nt":
56
+ import ctypes
57
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
58
+
59
+ class MEMORYSTATUSEX(ctypes.Structure):
60
+ _fields_ = [
61
+ ("dwLength", ctypes.c_ulong),
62
+ ("dwMemoryLoad", ctypes.c_ulong),
63
+ ("ullTotalPhys", ctypes.c_ulonglong),
64
+ ("ullAvailPhys", ctypes.c_ulonglong),
65
+ ("ullTotalPageFile", ctypes.c_ulonglong),
66
+ ("ullAvailPageFile", ctypes.c_ulonglong),
67
+ ("ullTotalVirtual", ctypes.c_ulonglong),
68
+ ("ullAvailVirtual", ctypes.c_ulonglong),
69
+ ("ullAvailExtendedVirtual", ctypes.c_ulonglong),
70
+ ]
71
+
72
+ mem = MEMORYSTATUSEX()
73
+ mem.dwLength = ctypes.sizeof(mem)
74
+ kernel32.GlobalMemoryStatusEx(ctypes.byref(mem))
75
+ total_ram_mb = mem.ullTotalPhys / (1024 * 1024)
76
+ available_ram_mb = mem.ullAvailPhys / (1024 * 1024)
77
+ else:
78
+ with open("/proc/meminfo") as f:
79
+ info = {}
80
+ for line in f:
81
+ parts = line.split()
82
+ if len(parts) >= 2:
83
+ info[parts[0].rstrip(":")] = int(parts[1])
84
+ total_ram_mb = info.get("MemTotal", 0) / 1024
85
+ available_ram_mb = info.get("MemAvailable", info.get("MemFree", 0)) / 1024
86
+ except Exception:
87
+ total_ram_mb = 0.0
88
+ available_ram_mb = 0.0
89
+
90
+ return {
91
+ "cpu_count": cpu_count,
92
+ "total_ram_mb": round(total_ram_mb),
93
+ "available_ram_mb": round(available_ram_mb),
94
+ }
95
+
96
+
97
+ # ═══════════════════════════════════════════════════════════════════════════
98
+ # Overhead history tracker
99
+ # ═══════════════════════════════════════════════════════════════════════════
100
+
101
+ @dataclass
102
+ class WindowOverheadRecord:
103
+ """Per-window overhead measurement."""
104
+ window_index: int = 0
105
+ total_ms: float = 0.0
106
+ llm_ms: float = 0.0
107
+ overhead_ms: float = 0.0
108
+ overhead_pct: float = 0.0
109
+ envelope_ms: float = 0.0
110
+ extraction_ms: float = 0.0
111
+ features_shed: list[str] = field(default_factory=list)
112
+ stages_skipped: list[int] = field(default_factory=list)
113
+
114
+
115
+ # ═══════════════════════════════════════════════════════════════════════════
116
+ # Extraction profile — what stages to run
117
+ # ═══════════════════════════════════════════════════════════════════════════
118
+
119
+ @dataclass
120
+ class ExtractionProfile:
121
+ """Recommended extraction configuration based on resource state."""
122
+ enable_stage_3: bool = True # GLiNER NER
123
+ enable_stage_4: bool = True # UIE relation extraction
124
+ enable_stage_5: bool = True # Discourse analysis
125
+ enable_stage_6: bool = False # LLM-assisted (always off by default)
126
+ max_facts_per_stage: int = 200
127
+
128
+
129
+ @dataclass
130
+ class EnvelopeProfile:
131
+ """Recommended envelope configuration based on resource state."""
132
+ enable_cross_encoder: bool = True
133
+ enable_ckf: bool = True
134
+ max_packing_facts: int = 500
135
+ embedding_batch_size: int = 32
136
+
137
+
138
+ @dataclass
139
+ class PromptEfficiency:
140
+ """LLM-side efficiency recommendations based on pipeline state.
141
+
142
+ These hints allow providers to optimize token usage, connection
143
+ management, and prompt caching — reducing cost and latency without
144
+ cutting any intelligence features.
145
+ """
146
+ deduplicate_facts: bool = True
147
+ cache_system_prompt: bool = True
148
+ compress_envelope: bool = False
149
+ reuse_connection: bool = True
150
+ estimated_cache_hit_pct: float = 0.0
151
+
152
+
153
+ # Throughput levels — graceful throttling without cutting intelligence
154
+ THROUGHPUT_NORMAL = "normal"
155
+ THROUGHPUT_THROTTLED = "throttled"
156
+ THROUGHPUT_CONSTRAINED = "constrained"
157
+
158
+ # Facts-per-stage at each throughput level
159
+ _THROUGHPUT_FACTS: dict[str, int] = {
160
+ THROUGHPUT_NORMAL: 200,
161
+ THROUGHPUT_THROTTLED: 150,
162
+ THROUGHPUT_CONSTRAINED: 100,
163
+ }
164
+
165
+ # Embedding batch sizes at each throughput level
166
+ _THROUGHPUT_BATCH: dict[str, int] = {
167
+ THROUGHPUT_NORMAL: 32,
168
+ THROUGHPUT_THROTTLED: 48,
169
+ THROUGHPUT_CONSTRAINED: 64,
170
+ }
171
+
172
+ # Max packing facts at each throughput level
173
+ _THROUGHPUT_PACKING: dict[str, int] = {
174
+ THROUGHPUT_NORMAL: 500,
175
+ THROUGHPUT_THROTTLED: 350,
176
+ THROUGHPUT_CONSTRAINED: 200,
177
+ }
178
+
179
+
180
+ # ═══════════════════════════════════════════════════════════════════════════
181
+ # Adaptive Allocator
182
+ # ═══════════════════════════════════════════════════════════════════════════
183
+
184
+
185
+ class AdaptiveAllocator:
186
+ """Dynamically tunes CRP pipeline based on real-time overhead and pressure.
187
+
188
+ The allocator observes per-window overhead ratios and resource pressure,
189
+ then adjusts throughput parameters and prompt efficiency hints to keep
190
+ overhead below the configured cap.
191
+
192
+ Core principle: ML extraction stages (GLiNER, UIE, Discourse) are
193
+ NEVER disabled — they are the protocol's core intelligence. Under
194
+ pressure, the system reduces fact counts and increases batch sizes
195
+ (graceful throttling) rather than cutting analytical capability.
196
+
197
+ Usage::
198
+
199
+ alloc = AdaptiveAllocator(
200
+ resource_manager=rm,
201
+ overhead_manager=om,
202
+ overhead_cap_pct=15.0,
203
+ )
204
+
205
+ # Before each dispatch — get recommended profiles
206
+ ext_profile = alloc.extraction_profile()
207
+ env_profile = alloc.envelope_profile()
208
+ efficiency = alloc.prompt_efficiency()
209
+
210
+ # After each dispatch — record overhead
211
+ alloc.record_window(total_ms=1200, llm_ms=1050,
212
+ envelope_ms=80, extraction_ms=50)
213
+
214
+ # Cleanup check
215
+ if alloc.should_unload_models():
216
+ for model in alloc.idle_models():
217
+ unload(model)
218
+ """
219
+
220
+ # Overhead smoothing: EWMA decay factor (0.3 = recent windows weighted more)
221
+ _EWMA_ALPHA = 0.3
222
+
223
+ # How many consecutive windows over cap before constrained throughput
224
+ _CONSECUTIVE_OVER_LIMIT = 3
225
+
226
+ def __init__(
227
+ self,
228
+ resource_manager: ResourceManager,
229
+ overhead_manager: OverheadBudgetManager,
230
+ *,
231
+ overhead_cap_pct: float = 15.0,
232
+ idle_model_timeout_s: float = 300.0,
233
+ max_history: int = 50,
234
+ ) -> None:
235
+ self._rm = resource_manager
236
+ self._om = overhead_manager
237
+ self._cap = overhead_cap_pct
238
+ self._idle_timeout = idle_model_timeout_s
239
+ self._lock = threading.Lock()
240
+
241
+ # Overhead history
242
+ self._history: list[WindowOverheadRecord] = []
243
+ self._max_history = max_history
244
+ self._window_count = 0
245
+
246
+ # EWMA smoothed overhead
247
+ self._ewma_overhead_pct: float = 0.0
248
+
249
+ # Consecutive windows over cap
250
+ self._consecutive_over: int = 0
251
+
252
+ # Throughput level — graceful throttling (never disables ML stages)
253
+ self._throughput_level: str = THROUGHPUT_NORMAL
254
+
255
+ # Hardware snapshot
256
+ self._hardware = detect_hardware()
257
+
258
+ logger.info(
259
+ "AdaptiveAllocator initialized: cap=%.1f%%, hardware=%s",
260
+ overhead_cap_pct, self._hardware,
261
+ )
262
+
263
+ # -- Record overhead per window ----------------------------------------
264
+
265
+ def record_window(
266
+ self,
267
+ total_ms: float,
268
+ llm_ms: float,
269
+ *,
270
+ envelope_ms: float = 0.0,
271
+ extraction_ms: float = 0.0,
272
+ ) -> WindowOverheadRecord:
273
+ """Record overhead from a completed window and adapt."""
274
+ overhead_ms = total_ms - llm_ms
275
+ overhead_pct = (overhead_ms / total_ms * 100) if total_ms > 0 else 0.0
276
+
277
+ with self._lock:
278
+ self._window_count += 1
279
+
280
+ # Update EWMA
281
+ if self._window_count == 1:
282
+ self._ewma_overhead_pct = overhead_pct
283
+ else:
284
+ self._ewma_overhead_pct = (
285
+ self._EWMA_ALPHA * overhead_pct
286
+ + (1 - self._EWMA_ALPHA) * self._ewma_overhead_pct
287
+ )
288
+
289
+ # Track consecutive over-cap windows
290
+ if overhead_pct > self._cap:
291
+ self._consecutive_over += 1
292
+ else:
293
+ self._consecutive_over = 0
294
+
295
+ # Build record
296
+ record = WindowOverheadRecord(
297
+ window_index=self._window_count,
298
+ total_ms=round(total_ms, 1),
299
+ llm_ms=round(llm_ms, 1),
300
+ overhead_ms=round(overhead_ms, 1),
301
+ overhead_pct=round(overhead_pct, 1),
302
+ envelope_ms=round(envelope_ms, 1),
303
+ extraction_ms=round(extraction_ms, 1),
304
+ features_shed=[
305
+ f for f in SHEDDING_CASCADE
306
+ if f not in PROTECTED_INTELLIGENCE
307
+ and not self._om.is_feature_enabled(f)
308
+ ],
309
+ stages_skipped=[], # ML stages are never skipped
310
+ )
311
+ self._history.append(record)
312
+ if len(self._history) > self._max_history:
313
+ self._history.pop(0)
314
+
315
+ # Update overhead manager
316
+ self._om.update_overhead(overhead_pct)
317
+
318
+ # Adapt throughput level (never disables ML stages)
319
+ self._adapt_throughput(overhead_pct)
320
+
321
+ return record
322
+
323
+ # -- Adaptive throughput scheduling ------------------------------------
324
+
325
+ def _adapt_throughput(self, overhead_pct: float) -> None:
326
+ """Adjust throughput level based on overhead trend.
327
+
328
+ Unlike stage disabling, this NEVER cuts ML intelligence features.
329
+ Instead it reduces fact counts and increases batch sizes to keep
330
+ overhead under cap while maintaining the full extraction pipeline.
331
+ """
332
+ if self._consecutive_over >= self._CONSECUTIVE_OVER_LIMIT:
333
+ # Sustained over-cap: constrain throughput
334
+ if self._throughput_level != THROUGHPUT_CONSTRAINED:
335
+ self._throughput_level = THROUGHPUT_CONSTRAINED
336
+ logger.info(
337
+ "Adaptive: throughput → constrained "
338
+ "(consecutive over cap: %d, ewma=%.1f%%)",
339
+ self._consecutive_over, self._ewma_overhead_pct,
340
+ )
341
+ elif overhead_pct > self._cap:
342
+ # Single window over cap: throttle
343
+ if self._throughput_level == THROUGHPUT_NORMAL:
344
+ self._throughput_level = THROUGHPUT_THROTTLED
345
+ logger.info(
346
+ "Adaptive: throughput → throttled "
347
+ "(overhead=%.1f%% > cap=%.1f%%)",
348
+ overhead_pct, self._cap,
349
+ )
350
+ elif self._ewma_overhead_pct < self._cap * 0.7:
351
+ # Well under budget: restore throughput one step
352
+ if self._throughput_level == THROUGHPUT_CONSTRAINED:
353
+ self._throughput_level = THROUGHPUT_THROTTLED
354
+ logger.info(
355
+ "Adaptive: throughput → throttled (restoring, ewma=%.1f%%)",
356
+ self._ewma_overhead_pct,
357
+ )
358
+ elif self._throughput_level == THROUGHPUT_THROTTLED:
359
+ self._throughput_level = THROUGHPUT_NORMAL
360
+ logger.info(
361
+ "Adaptive: throughput → normal (restored, ewma=%.1f%%)",
362
+ self._ewma_overhead_pct,
363
+ )
364
+
365
+ # -- Profile generation ------------------------------------------------
366
+
367
+ def extraction_profile(self) -> ExtractionProfile:
368
+ """Get recommended extraction profile based on current state.
369
+
370
+ ML stages (3=GLiNER, 4=UIE, 5=Discourse) are ALWAYS enabled.
371
+ Under pressure, throughput is throttled via max_facts_per_stage.
372
+ """
373
+ with self._lock:
374
+ pressure = self._rm.snapshot().pressure_level
375
+
376
+ # ML stages are ALWAYS enabled — they are core intelligence
377
+ profile = ExtractionProfile(
378
+ enable_stage_3=True, # GLiNER NER — always on
379
+ enable_stage_4=True, # UIE relation extraction — always on
380
+ enable_stage_5=True, # Discourse analysis — always on
381
+ enable_stage_6=False, # LLM-assisted — off by default
382
+ )
383
+
384
+ # Throttle throughput based on pressure + throughput level
385
+ if pressure in ("high", "critical"):
386
+ profile.max_facts_per_stage = _THROUGHPUT_FACTS[THROUGHPUT_CONSTRAINED]
387
+ elif pressure == "medium":
388
+ profile.max_facts_per_stage = _THROUGHPUT_FACTS[THROUGHPUT_THROTTLED]
389
+ else:
390
+ profile.max_facts_per_stage = _THROUGHPUT_FACTS[self._throughput_level]
391
+
392
+ return profile
393
+
394
+ def envelope_profile(self) -> EnvelopeProfile:
395
+ """Get recommended envelope profile based on current state.
396
+
397
+ CKF (Contextual Knowledge Fusion) is ALWAYS enabled — it is
398
+ core to CRP's knowledge retrieval. Under pressure, batch sizes
399
+ increase and packing limits decrease to reduce overhead.
400
+ """
401
+ with self._lock:
402
+ pressure = self._rm.snapshot().pressure_level
403
+
404
+ profile = EnvelopeProfile(
405
+ enable_cross_encoder=self._om.is_feature_enabled("cross_encoder"),
406
+ enable_ckf=True, # CKF is always enabled — core intelligence
407
+ max_packing_facts=_THROUGHPUT_PACKING[self._throughput_level],
408
+ embedding_batch_size=_THROUGHPUT_BATCH[self._throughput_level],
409
+ )
410
+
411
+ # Pressure overrides: tighter limits but CKF stays on
412
+ if pressure in ("high", "critical"):
413
+ profile.max_packing_facts = _THROUGHPUT_PACKING[THROUGHPUT_CONSTRAINED]
414
+ profile.embedding_batch_size = _THROUGHPUT_BATCH[THROUGHPUT_CONSTRAINED]
415
+ elif pressure == "medium":
416
+ profile.max_packing_facts = _THROUGHPUT_PACKING[THROUGHPUT_THROTTLED]
417
+ profile.embedding_batch_size = _THROUGHPUT_BATCH[THROUGHPUT_THROTTLED]
418
+
419
+ # If cross_encoder was shed by OverheadBudgetManager, respect that
420
+ if not self._om.is_feature_enabled("cross_encoder"):
421
+ profile.enable_cross_encoder = False
422
+
423
+ return profile
424
+
425
+ # -- Model lifecycle decisions -----------------------------------------
426
+
427
+ def should_unload_models(self) -> bool:
428
+ """Check if idle models should be unloaded."""
429
+ pressure = self._rm.snapshot().pressure_level
430
+ if pressure in ("high", "critical"):
431
+ return True
432
+ idle = self._rm.get_idle_models(self._idle_timeout)
433
+ return len(idle) > 0
434
+
435
+ def idle_models(self) -> list[str]:
436
+ """Get list of models eligible for unloading."""
437
+ pressure = self._rm.snapshot().pressure_level
438
+ if pressure == "critical":
439
+ # Under critical pressure, use shorter timeout
440
+ return self._rm.get_idle_models(self._idle_timeout / 3)
441
+ return self._rm.get_idle_models(self._idle_timeout)
442
+
443
+ def should_run_gc(self) -> bool:
444
+ """Check if garbage collection should run."""
445
+ pressure = self._rm.snapshot().pressure_level
446
+ return pressure in ("medium", "high", "critical")
447
+
448
+ # -- Overhead queries --------------------------------------------------
449
+
450
+ @property
451
+ def ewma_overhead_pct(self) -> float:
452
+ return self._ewma_overhead_pct
453
+
454
+ @property
455
+ def window_count(self) -> int:
456
+ return self._window_count
457
+
458
+ @property
459
+ def consecutive_over_cap(self) -> int:
460
+ return self._consecutive_over
461
+
462
+ @property
463
+ def disabled_stages(self) -> set[int]:
464
+ """ML stages are never disabled — always returns empty set."""
465
+ return set()
466
+
467
+ @property
468
+ def throughput_level(self) -> str:
469
+ """Current throughput level: normal, throttled, or constrained."""
470
+ return self._throughput_level
471
+
472
+ @property
473
+ def hardware(self) -> dict[str, int | float]:
474
+ return dict(self._hardware)
475
+
476
+ def overhead_trend(self, last_n: int = 5) -> list[float]:
477
+ """Return recent overhead percentages."""
478
+ with self._lock:
479
+ return [r.overhead_pct for r in self._history[-last_n:]]
480
+
481
+ # -- Prompt efficiency -------------------------------------------------
482
+
483
+ def prompt_efficiency(self) -> PromptEfficiency:
484
+ """Get LLM prompt efficiency recommendations.
485
+
486
+ After 2+ windows the system prompt is stable and should be cached.
487
+ Under constrained throughput, envelope compression is recommended.
488
+ """
489
+ with self._lock:
490
+ cache_enabled = self._window_count >= 2
491
+ compress = self._throughput_level == THROUGHPUT_CONSTRAINED
492
+ cache_pct = min(90.0, self._window_count * 15.0) if cache_enabled else 0.0
493
+
494
+ return PromptEfficiency(
495
+ deduplicate_facts=True,
496
+ cache_system_prompt=cache_enabled,
497
+ compress_envelope=compress,
498
+ reuse_connection=True,
499
+ estimated_cache_hit_pct=round(cache_pct, 1),
500
+ )
501
+
502
+ # -- Summary -----------------------------------------------------------
503
+
504
+ def summary(self) -> dict:
505
+ """Full allocator state for diagnostics."""
506
+ with self._lock:
507
+ return {
508
+ "overhead_cap_pct": self._cap,
509
+ "ewma_overhead_pct": round(self._ewma_overhead_pct, 1),
510
+ "window_count": self._window_count,
511
+ "consecutive_over_cap": self._consecutive_over,
512
+ "throughput_level": self._throughput_level,
513
+ "disabled_stages": [], # ML stages are never disabled
514
+ "hardware": self._hardware,
515
+ "feature_shedding": self._om.enabled_features,
516
+ "resource_pressure": self._rm.snapshot().pressure_level,
517
+ "overhead_trend": [r.overhead_pct for r in self._history[-10:]],
518
+ "prompt_efficiency": {
519
+ "cache_system_prompt": self._window_count >= 2,
520
+ "estimated_cache_hit_pct": round(
521
+ min(90.0, self._window_count * 15.0)
522
+ if self._window_count >= 2 else 0.0, 1,
523
+ ),
524
+ },
525
+ }