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,157 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Voice profile — tone, terminology, style extraction from first window (§04 §3.5.1)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from collections import Counter
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class VoiceProfile:
14
+ """Captured voice characteristics from the first generation window."""
15
+
16
+ tone: str # e.g. "formal", "casual", "technical", "mixed"
17
+ avg_sentence_length: float
18
+ vocabulary_level: str # "basic", "intermediate", "advanced", "technical"
19
+ person: str # "first", "second", "third", "mixed"
20
+ active_voice_ratio: float # 0.0–1.0
21
+ key_terminology: list[str] # domain terms used
22
+ style_markers: dict[str, object] = field(default_factory=dict)
23
+
24
+ def to_dict(self) -> dict[str, object]:
25
+ return {
26
+ "tone": self.tone,
27
+ "avg_sentence_length": self.avg_sentence_length,
28
+ "vocabulary_level": self.vocabulary_level,
29
+ "person": self.person,
30
+ "active_voice_ratio": self.active_voice_ratio,
31
+ "key_terminology": self.key_terminology,
32
+ "style_markers": self.style_markers,
33
+ }
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict[str, object]) -> VoiceProfile:
37
+ return cls(
38
+ tone=str(data.get("tone", "neutral")),
39
+ avg_sentence_length=float(data.get("avg_sentence_length", 0)), # type: ignore[arg-type]
40
+ vocabulary_level=str(data.get("vocabulary_level", "intermediate")),
41
+ person=str(data.get("person", "third")),
42
+ active_voice_ratio=float(data.get("active_voice_ratio", 0.5)), # type: ignore[arg-type]
43
+ key_terminology=list(data.get("key_terminology", [])), # type: ignore[call-overload]
44
+ style_markers=dict(data.get("style_markers", {})), # type: ignore[call-overload]
45
+ )
46
+
47
+
48
+ # ── Extraction helpers ───────────────────────────────────────────
49
+
50
+ _FORMAL_MARKERS = {
51
+ "therefore", "consequently", "furthermore", "moreover", "nevertheless",
52
+ "notwithstanding", "henceforth", "whereby", "wherein", "herein",
53
+ "accordingly", "albeit", "hitherto", "therein",
54
+ }
55
+
56
+ _CASUAL_MARKERS = {
57
+ "okay", "ok", "cool", "awesome", "basically", "literally",
58
+ "gonna", "wanna", "kinda", "sorta", "yeah", "nope", "stuff",
59
+ "thing", "actually", "pretty much", "like",
60
+ }
61
+
62
+ _TECHNICAL_TERMS_PATTERN = re.compile(
63
+ r"\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b" # CamelCase
64
+ r"|\b[a-z]+_[a-z]+\b" # snake_case
65
+ r"|\b[A-Z]{2,}\b" # ACRONYMS
66
+ r"|\b\w+(?:tion|ment|ness|ity|ism|ize|ify)\b" # nominalizations
67
+ )
68
+
69
+ _PASSIVE_PATTERN = re.compile(
70
+ r"\b(?:is|are|was|were|be|been|being)\s+(?:\w+\s+)*?\w+(?:ed|en)\b",
71
+ re.IGNORECASE,
72
+ )
73
+
74
+ _FIRST_PERSON = re.compile(r"\b(?:I|me|my|mine|we|us|our|ours)\b", re.IGNORECASE)
75
+ _SECOND_PERSON = re.compile(r"\b(?:you|your|yours)\b", re.IGNORECASE)
76
+ _THIRD_PERSON = re.compile(r"\b(?:he|she|it|they|him|her|them|his|its|their)\b", re.IGNORECASE)
77
+
78
+
79
+ def extract_voice_profile(text: str) -> VoiceProfile:
80
+ """Extract a voice profile from the first window's output (§04 §3.5.1)."""
81
+ words = text.lower().split()
82
+ word_count = max(1, len(words))
83
+ word_set = set(words)
84
+
85
+ sentences = [s.strip() for s in re.split(r"[.!?]+\s*", text) if s.strip()]
86
+ sentence_count = max(1, len(sentences))
87
+
88
+ # Tone classification
89
+ formal_count = len(word_set & _FORMAL_MARKERS)
90
+ casual_count = len(word_set & _CASUAL_MARKERS)
91
+ tech_matches = _TECHNICAL_TERMS_PATTERN.findall(text)
92
+
93
+ if len(tech_matches) > word_count * 0.05:
94
+ tone = "technical"
95
+ elif formal_count > casual_count:
96
+ tone = "formal"
97
+ elif casual_count > formal_count:
98
+ tone = "casual"
99
+ else:
100
+ tone = "neutral"
101
+
102
+ # Average sentence length
103
+ avg_sentence_length = word_count / sentence_count
104
+
105
+ # Vocabulary level
106
+ unique_ratio = len(word_set) / word_count
107
+ avg_word_len = sum(len(w) for w in words) / word_count
108
+ if avg_word_len > 7 and unique_ratio > 0.6:
109
+ vocabulary_level = "advanced"
110
+ elif avg_word_len > 5.5 or unique_ratio > 0.5:
111
+ vocabulary_level = "intermediate"
112
+ elif len(tech_matches) > 5:
113
+ vocabulary_level = "technical"
114
+ else:
115
+ vocabulary_level = "basic"
116
+
117
+ # Person detection
118
+ first = len(_FIRST_PERSON.findall(text))
119
+ second = len(_SECOND_PERSON.findall(text))
120
+ third = len(_THIRD_PERSON.findall(text))
121
+ total_person = first + second + third
122
+ if total_person == 0:
123
+ person = "third"
124
+ elif first > second and first > third:
125
+ person = "first"
126
+ elif second > first and second > third:
127
+ person = "second"
128
+ elif first > 0 and second > 0:
129
+ person = "mixed"
130
+ else:
131
+ person = "third"
132
+
133
+ # Active voice ratio
134
+ passive_count = len(_PASSIVE_PATTERN.findall(text))
135
+ active_ratio = 1.0 - (passive_count / sentence_count) if sentence_count > 0 else 0.5
136
+ active_ratio = max(0.0, min(1.0, active_ratio))
137
+
138
+ # Key terminology (most frequent technical-looking terms)
139
+ term_counter: Counter[str] = Counter()
140
+ for match in tech_matches:
141
+ term_counter[match] += 1
142
+ key_terms = [t for t, _ in term_counter.most_common(20)]
143
+
144
+ return VoiceProfile(
145
+ tone=tone,
146
+ avg_sentence_length=round(avg_sentence_length, 1),
147
+ vocabulary_level=vocabulary_level,
148
+ person=person,
149
+ active_voice_ratio=round(active_ratio, 2),
150
+ key_terminology=key_terms,
151
+ style_markers={
152
+ "formal_markers": formal_count,
153
+ "casual_markers": casual_count,
154
+ "tech_terms": len(tech_matches),
155
+ "unique_word_ratio": round(unique_ratio, 3),
156
+ },
157
+ )
crp/core/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Core protocol — orchestrator, task intent, windows, sessions, config, errors."""
4
+
5
+ from crp.core.batch import BatchResult, dispatch_batch, ingest_batch
6
+ from crp.core.config import ConfigurationResolver, CRPConfig
7
+ from crp.core.errors import (
8
+ BudgetExhaustedError,
9
+ CRPError,
10
+ ProviderError,
11
+ SessionClosedError,
12
+ SessionExpiredError,
13
+ )
14
+ from crp.core.idempotency import CachedResult, RequestDeduplicator, SessionLock
15
+ from crp.core.orchestrator import (
16
+ CRPOrchestrator,
17
+ ExtractionResult,
18
+ StreamEvent,
19
+ )
20
+ from crp.core.session import (
21
+ CostEstimate,
22
+ EnvelopePreview,
23
+ QualityReport,
24
+ RemainingBudget,
25
+ SecurityFlags,
26
+ SessionHandle,
27
+ SessionStatus,
28
+ )
29
+ from crp.core.task_intent import TaskIntent
30
+ from crp.core.window import WindowDAG, WindowMetrics, WindowNode, WindowState
31
+
32
+ __all__ = [
33
+ # Orchestrator
34
+ "CRPOrchestrator",
35
+ "ExtractionResult",
36
+ "StreamEvent",
37
+ # Config
38
+ "ConfigurationResolver",
39
+ "CRPConfig",
40
+ # Session
41
+ "SessionHandle",
42
+ "SessionStatus",
43
+ "CostEstimate",
44
+ "EnvelopePreview",
45
+ "QualityReport",
46
+ "RemainingBudget",
47
+ "SecurityFlags",
48
+ # Task
49
+ "TaskIntent",
50
+ # Window
51
+ "WindowDAG",
52
+ "WindowMetrics",
53
+ "WindowNode",
54
+ "WindowState",
55
+ # Errors
56
+ "CRPError",
57
+ "BudgetExhaustedError",
58
+ "ProviderError",
59
+ "SessionClosedError",
60
+ "SessionExpiredError",
61
+ # Batch
62
+ "BatchResult",
63
+ "dispatch_batch",
64
+ "ingest_batch",
65
+ # Idempotency
66
+ "CachedResult",
67
+ "RequestDeduplicator",
68
+ "SessionLock",
69
+ ]
crp/core/batch.py ADDED
@@ -0,0 +1,77 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Batch dispatch and ingest operations (§6.6).
4
+
5
+ dispatch_batch: Fan-out multiple intents in parallel.
6
+ ingest_batch: Batch ingestion with boundary reconciliation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+
16
+ @dataclass
17
+ class BatchResult:
18
+ """Result for one item in a batch operation."""
19
+
20
+ index: int = 0
21
+ output: str = ""
22
+ success: bool = True
23
+ error: str | None = None
24
+ facts_extracted: int = 0
25
+
26
+
27
+ def dispatch_batch(
28
+ intents: list[dict[str, str]],
29
+ dispatch_fn: Callable[[str, str], tuple[str, Any]] | None = None,
30
+ ) -> list[BatchResult]:
31
+ """Batch dispatch with sequential execution (parallel fan-out ready).
32
+
33
+ Each intent should have keys: "system_prompt", "task_input".
34
+ """
35
+ results: list[BatchResult] = []
36
+ for i, intent in enumerate(intents):
37
+ sys_prompt = intent.get("system_prompt", "")
38
+ task_input = intent.get("task_input", "")
39
+ if dispatch_fn:
40
+ try:
41
+ output, report = dispatch_fn(sys_prompt, task_input)
42
+ results.append(BatchResult(index=i, output=output))
43
+ except Exception as exc:
44
+ results.append(BatchResult(
45
+ index=i, success=False, error=str(exc),
46
+ ))
47
+ else:
48
+ results.append(BatchResult(
49
+ index=i, output="[no dispatch_fn]",
50
+ ))
51
+ return results
52
+
53
+
54
+ def ingest_batch(
55
+ texts: list[str],
56
+ extract_fn: Callable[[str, str], list[Any]] | None = None,
57
+ task_intent: str = "",
58
+ ) -> list[BatchResult]:
59
+ """Batch ingestion with per-item extraction.
60
+
61
+ Returns extraction results per text item.
62
+ """
63
+ results: list[BatchResult] = []
64
+ for i, text in enumerate(texts):
65
+ if extract_fn:
66
+ try:
67
+ facts = extract_fn(text, task_intent)
68
+ results.append(BatchResult(
69
+ index=i, facts_extracted=len(facts),
70
+ ))
71
+ except Exception as exc:
72
+ results.append(BatchResult(
73
+ index=i, success=False, error=str(exc),
74
+ ))
75
+ else:
76
+ results.append(BatchResult(index=i, facts_extracted=0))
77
+ return results
@@ -0,0 +1,116 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Circuit breaker for LLM provider calls (§audit H4).
4
+
5
+ Implements the CLOSED → OPEN → HALF-OPEN pattern to prevent
6
+ cascading failures when a provider is unavailable.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import threading
13
+ import time
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+
17
+ logger = logging.getLogger("crp.core.circuit_breaker")
18
+
19
+
20
+ class CircuitState(Enum):
21
+ CLOSED = "closed" # Normal operation
22
+ OPEN = "open" # Failing — reject calls immediately
23
+ HALF_OPEN = "half_open" # Testing recovery — allow one probe call
24
+
25
+
26
+ @dataclass
27
+ class CircuitBreakerConfig:
28
+ """Configuration for the circuit breaker."""
29
+ failure_threshold: int = 5 # consecutive failures to trip OPEN
30
+ recovery_timeout: float = 30.0 # seconds before OPEN → HALF-OPEN
31
+ success_threshold: int = 2 # successes in HALF-OPEN to close
32
+
33
+
34
+ class CircuitBreaker:
35
+ """Thread-safe circuit breaker for LLM providers."""
36
+
37
+ def __init__(self, config: CircuitBreakerConfig | None = None) -> None:
38
+ self._config = config or CircuitBreakerConfig()
39
+ self._state = CircuitState.CLOSED
40
+ self._failure_count = 0
41
+ self._success_count = 0
42
+ self._last_failure_time: float = 0.0
43
+ self._lock = threading.Lock()
44
+ self._half_open_probing = False # Only allow one probe (§audit2 CB-M10)
45
+
46
+ @property
47
+ def state(self) -> CircuitState:
48
+ """Current circuit state (may transition OPEN → HALF-OPEN on read)."""
49
+ with self._lock:
50
+ if self._state == CircuitState.OPEN:
51
+ elapsed = time.monotonic() - self._last_failure_time
52
+ if elapsed >= self._config.recovery_timeout:
53
+ self._state = CircuitState.HALF_OPEN
54
+ self._success_count = 0
55
+ self._half_open_probing = False
56
+ logger.info("Circuit breaker → HALF-OPEN (recovery timeout elapsed)")
57
+ return self._state
58
+
59
+ def allow_request(self) -> bool:
60
+ """Check if a request should be allowed through."""
61
+ with self._lock:
62
+ current = self._state
63
+ if current == CircuitState.OPEN:
64
+ elapsed = time.monotonic() - self._last_failure_time
65
+ if elapsed >= self._config.recovery_timeout:
66
+ self._state = CircuitState.HALF_OPEN
67
+ self._success_count = 0
68
+ self._half_open_probing = False
69
+ current = CircuitState.HALF_OPEN
70
+ if current == CircuitState.CLOSED:
71
+ return True
72
+ if current == CircuitState.HALF_OPEN:
73
+ if self._half_open_probing:
74
+ return False # Only one probe at a time (§audit2 CB-M10)
75
+ self._half_open_probing = True
76
+ return True
77
+ return False # OPEN — reject
78
+
79
+ def record_success(self) -> None:
80
+ """Record a successful call."""
81
+ with self._lock:
82
+ if self._state == CircuitState.HALF_OPEN:
83
+ self._success_count += 1
84
+ self._half_open_probing = False
85
+ if self._success_count >= self._config.success_threshold:
86
+ self._state = CircuitState.CLOSED
87
+ self._failure_count = 0
88
+ logger.info("Circuit breaker → CLOSED (recovery confirmed)")
89
+ else:
90
+ self._failure_count = 0
91
+
92
+ def record_failure(self) -> None:
93
+ """Record a failed call."""
94
+ with self._lock:
95
+ self._failure_count += 1
96
+ self._last_failure_time = time.monotonic()
97
+
98
+ if self._state == CircuitState.HALF_OPEN:
99
+ # Single failure in half-open immediately trips back to open
100
+ self._state = CircuitState.OPEN
101
+ self._half_open_probing = False
102
+ logger.warning("Circuit breaker → OPEN (half-open probe failed)")
103
+ elif self._failure_count >= self._config.failure_threshold:
104
+ self._state = CircuitState.OPEN
105
+ logger.warning(
106
+ "Circuit breaker → OPEN after %d consecutive failures",
107
+ self._failure_count,
108
+ )
109
+
110
+ def reset(self) -> None:
111
+ """Reset to closed state."""
112
+ with self._lock:
113
+ self._state = CircuitState.CLOSED
114
+ self._failure_count = 0
115
+ self._success_count = 0
116
+ self._half_open_probing = False