agentic-exception-sdk 1.1.3__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 (106) hide show
  1. agentic_exception_sdk/__init__.py +308 -0
  2. agentic_exception_sdk/__init__.pyi +38 -0
  3. agentic_exception_sdk/budget/__init__.py +14 -0
  4. agentic_exception_sdk/budget/__init__.pyi +5 -0
  5. agentic_exception_sdk/budget/models.py +48 -0
  6. agentic_exception_sdk/budget/models.pyi +14 -0
  7. agentic_exception_sdk/budget/streaming.py +144 -0
  8. agentic_exception_sdk/budget/streaming.pyi +27 -0
  9. agentic_exception_sdk/budget/watchdog.py +285 -0
  10. agentic_exception_sdk/budget/watchdog.pyi +24 -0
  11. agentic_exception_sdk/bundle.py +102 -0
  12. agentic_exception_sdk/bundle.pyi +37 -0
  13. agentic_exception_sdk/config/__init__.py +10 -0
  14. agentic_exception_sdk/config/__init__.pyi +3 -0
  15. agentic_exception_sdk/config/validate.py +74 -0
  16. agentic_exception_sdk/config/validate.pyi +11 -0
  17. agentic_exception_sdk/escalation/__init__.py +22 -0
  18. agentic_exception_sdk/escalation/__init__.pyi +5 -0
  19. agentic_exception_sdk/escalation/checkpoint.py +116 -0
  20. agentic_exception_sdk/escalation/checkpoint.pyi +13 -0
  21. agentic_exception_sdk/escalation/handlers.py +27 -0
  22. agentic_exception_sdk/escalation/handlers.pyi +7 -0
  23. agentic_exception_sdk/escalation/router.py +137 -0
  24. agentic_exception_sdk/escalation/router.pyi +22 -0
  25. agentic_exception_sdk/multi_agent/__init__.py +32 -0
  26. agentic_exception_sdk/multi_agent/__init__.pyi +6 -0
  27. agentic_exception_sdk/multi_agent/consensus.py +139 -0
  28. agentic_exception_sdk/multi_agent/consensus.pyi +18 -0
  29. agentic_exception_sdk/multi_agent/parallel.py +204 -0
  30. agentic_exception_sdk/multi_agent/parallel.pyi +35 -0
  31. agentic_exception_sdk/multi_agent/router.py +131 -0
  32. agentic_exception_sdk/multi_agent/router.pyi +14 -0
  33. agentic_exception_sdk/multi_agent/sla.py +63 -0
  34. agentic_exception_sdk/multi_agent/sla.pyi +10 -0
  35. agentic_exception_sdk/noop.py +27 -0
  36. agentic_exception_sdk/noop.pyi +5 -0
  37. agentic_exception_sdk/observability/__init__.py +24 -0
  38. agentic_exception_sdk/observability/__init__.pyi +6 -0
  39. agentic_exception_sdk/observability/metrics.py +189 -0
  40. agentic_exception_sdk/observability/metrics.pyi +36 -0
  41. agentic_exception_sdk/observability/otel.py +211 -0
  42. agentic_exception_sdk/observability/otel.pyi +9 -0
  43. agentic_exception_sdk/observability/sink.py +68 -0
  44. agentic_exception_sdk/observability/sink.pyi +12 -0
  45. agentic_exception_sdk/observability/structured_log.py +110 -0
  46. agentic_exception_sdk/observability/structured_log.pyi +8 -0
  47. agentic_exception_sdk/persistence/__init__.py +27 -0
  48. agentic_exception_sdk/persistence/__init__.pyi +35 -0
  49. agentic_exception_sdk/persistence/attestation.py +36 -0
  50. agentic_exception_sdk/persistence/attestation.pyi +8 -0
  51. agentic_exception_sdk/persistence/availability.py +14 -0
  52. agentic_exception_sdk/persistence/availability.pyi +7 -0
  53. agentic_exception_sdk/persistence/null_provider.py +71 -0
  54. agentic_exception_sdk/persistence/null_provider.pyi +19 -0
  55. agentic_exception_sdk/persistence/provider.py +70 -0
  56. agentic_exception_sdk/persistence/provider.pyi +33 -0
  57. agentic_exception_sdk/propagation/__init__.py +39 -0
  58. agentic_exception_sdk/propagation/__init__.pyi +5 -0
  59. agentic_exception_sdk/propagation/bus.py +162 -0
  60. agentic_exception_sdk/propagation/bus.pyi +30 -0
  61. agentic_exception_sdk/propagation/dlq.py +173 -0
  62. agentic_exception_sdk/propagation/dlq.pyi +28 -0
  63. agentic_exception_sdk/propagation/protocol.py +129 -0
  64. agentic_exception_sdk/propagation/protocol.pyi +10 -0
  65. agentic_exception_sdk/py.typed +0 -0
  66. agentic_exception_sdk/resilience/__init__.py +59 -0
  67. agentic_exception_sdk/resilience/__init__.pyi +7 -0
  68. agentic_exception_sdk/resilience/circuit_breaker.py +650 -0
  69. agentic_exception_sdk/resilience/circuit_breaker.pyi +58 -0
  70. agentic_exception_sdk/resilience/compensating.py +97 -0
  71. agentic_exception_sdk/resilience/compensating.pyi +9 -0
  72. agentic_exception_sdk/resilience/fallback.py +88 -0
  73. agentic_exception_sdk/resilience/fallback.pyi +16 -0
  74. agentic_exception_sdk/resilience/recovery.py +50 -0
  75. agentic_exception_sdk/resilience/recovery.pyi +11 -0
  76. agentic_exception_sdk/resilience/retry.py +455 -0
  77. agentic_exception_sdk/resilience/retry.pyi +62 -0
  78. agentic_exception_sdk/resilience/wrap.py +1073 -0
  79. agentic_exception_sdk/resilience/wrap.pyi +28 -0
  80. agentic_exception_sdk/taxonomy/__init__.py +56 -0
  81. agentic_exception_sdk/taxonomy/__init__.pyi +7 -0
  82. agentic_exception_sdk/taxonomy/classifier.py +216 -0
  83. agentic_exception_sdk/taxonomy/classifier.pyi +8 -0
  84. agentic_exception_sdk/taxonomy/enums.py +77 -0
  85. agentic_exception_sdk/taxonomy/enums.pyi +22 -0
  86. agentic_exception_sdk/taxonomy/envelope.py +223 -0
  87. agentic_exception_sdk/taxonomy/envelope.pyi +29 -0
  88. agentic_exception_sdk/taxonomy/errors.py +173 -0
  89. agentic_exception_sdk/taxonomy/errors.pyi +34 -0
  90. agentic_exception_sdk/taxonomy/owasp.py +55 -0
  91. agentic_exception_sdk/taxonomy/owasp.pyi +19 -0
  92. agentic_exception_sdk/validation/__init__.py +27 -0
  93. agentic_exception_sdk/validation/__init__.pyi +6 -0
  94. agentic_exception_sdk/validation/gates.py +93 -0
  95. agentic_exception_sdk/validation/gates.pyi +16 -0
  96. agentic_exception_sdk/validation/guard_rails.py +114 -0
  97. agentic_exception_sdk/validation/guard_rails.pyi +18 -0
  98. agentic_exception_sdk/validation/rules_version.py +5 -0
  99. agentic_exception_sdk/validation/rules_version.pyi +3 -0
  100. agentic_exception_sdk/validation/trust_boundary.py +420 -0
  101. agentic_exception_sdk/validation/trust_boundary.pyi +18 -0
  102. agentic_exception_sdk-1.1.3.dist-info/METADATA +291 -0
  103. agentic_exception_sdk-1.1.3.dist-info/RECORD +106 -0
  104. agentic_exception_sdk-1.1.3.dist-info/WHEEL +5 -0
  105. agentic_exception_sdk-1.1.3.dist-info/licenses/LICENSE +21 -0
  106. agentic_exception_sdk-1.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,285 @@
1
+ """Budget watchdog — enforces agent resource budgets with concurrency-safe reservations.
2
+
3
+ Budget consumption semantics (fixed):
4
+ - consume_call(): once per resilient()/async_resilient() invocation.
5
+ - failure_budget: incremented once per failed attempt, including retries.
6
+ - Token/cost: check-and-reserve is one lock-protected atomic operation.
7
+ Calls that cannot reserve capacity fail before starting upstream work,
8
+ preventing N * delta overspend under contention.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import threading
15
+ import time
16
+
17
+ from agentic_exception_sdk.budget.models import AgentBudget
18
+ from agentic_exception_sdk.taxonomy.errors import BudgetExhaustedError
19
+
20
+ _log = logging.getLogger(__name__)
21
+
22
+ __all__ = ["BudgetWatchdog"]
23
+
24
+
25
+ class BudgetWatchdog:
26
+ """Tracks and enforces agent resource budgets with concurrency-safe atomic reservations.
27
+
28
+ Sync and async paths share a threading.RLock so mixed-concurrency callers
29
+ cannot race with each other.
30
+
31
+ All check-and-reserve operations are atomic within their respective lock so
32
+ that concurrent async_resilient() executions cannot overspend the budget
33
+ through races (N * delta overspend prevention).
34
+
35
+ Args:
36
+ budget: The AgentBudget configuration specifying ceiling values.
37
+ """
38
+
39
+ def __init__(self, budget: AgentBudget) -> None:
40
+ self._budget = budget
41
+ self._start_time: float = time.monotonic()
42
+ self._call_count: int = 0
43
+ self._failure_count: int = 0
44
+ self._input_tokens_used: int = 0
45
+ self._output_tokens_used: int = 0
46
+ self._cost_micros_used: int = 0
47
+ self._lock = threading.RLock()
48
+
49
+ def consume_call(self) -> None:
50
+ """Record one tool call invocation and check max_tool_calls ceiling.
51
+
52
+ This must be called once per resilient()/async_resilient() invocation.
53
+
54
+ Raises:
55
+ BudgetExhaustedError: If max_tool_calls ceiling is exceeded.
56
+ BudgetWarningError: Not currently raised here (implemented at soft-limit).
57
+ """
58
+ with self._lock:
59
+ self._call_count += 1
60
+ if (
61
+ self._budget.max_tool_calls is not None
62
+ and self._call_count > self._budget.max_tool_calls
63
+ ):
64
+ raise BudgetExhaustedError(
65
+ f"tool call budget exhausted: {self._call_count} > "
66
+ f"{self._budget.max_tool_calls} max_tool_calls"
67
+ )
68
+
69
+ def record_failure(self) -> None:
70
+ """Increment failure count once per failed attempt including retries.
71
+
72
+ Raises:
73
+ BudgetExhaustedError: If failure_budget ceiling is exceeded.
74
+ """
75
+ with self._lock:
76
+ self._failure_count += 1
77
+ if (
78
+ self._budget.failure_budget is not None
79
+ and self._failure_count >= self._budget.failure_budget
80
+ ):
81
+ raise BudgetExhaustedError(
82
+ f"failure budget exhausted: {self._failure_count} failures >= "
83
+ f"{self._budget.failure_budget} failure_budget"
84
+ )
85
+
86
+ def check_time(self) -> None:
87
+ """Check wall-clock time budget.
88
+
89
+ Raises:
90
+ BudgetExhaustedError: If max_seconds ceiling has been exceeded.
91
+ """
92
+ if self._budget.max_seconds is None:
93
+ return
94
+ elapsed = time.monotonic() - self._start_time
95
+ if elapsed > self._budget.max_seconds:
96
+ raise BudgetExhaustedError(
97
+ f"time budget exhausted: {elapsed:.2f}s > {self._budget.max_seconds}s max_seconds"
98
+ )
99
+
100
+ def check_before_sleep(self, sleep_seconds: float) -> None:
101
+ """Check whether the remaining time budget can accommodate a proposed sleep.
102
+
103
+ Called by the retry policy before each backoff sleep.
104
+
105
+ Args:
106
+ sleep_seconds: Proposed sleep duration in seconds.
107
+
108
+ Raises:
109
+ BudgetExhaustedError: If the remaining time budget cannot cover the sleep.
110
+ """
111
+ if self._budget.max_seconds is None:
112
+ return
113
+ elapsed = time.monotonic() - self._start_time
114
+ remaining = self._budget.max_seconds - elapsed
115
+ if sleep_seconds > remaining:
116
+ raise BudgetExhaustedError(
117
+ f"retry sleep ({sleep_seconds:.2f}s) would exceed remaining time budget "
118
+ f"({remaining:.2f}s)"
119
+ )
120
+
121
+ def reserve_tokens(
122
+ self,
123
+ *,
124
+ input_tokens: int = 0,
125
+ output_tokens: int = 0,
126
+ cost_micros_usd: int = 0,
127
+ ) -> None:
128
+ """Atomically check and reserve token/cost capacity before an LLM call.
129
+
130
+ Check-and-reserve is one lock-protected operation to prevent N * delta
131
+ overspend under concurrent async calls.
132
+
133
+ Args:
134
+ input_tokens: Input tokens to reserve.
135
+ output_tokens: Output tokens to reserve.
136
+ cost_micros_usd: Cost in integer micro-USD to reserve.
137
+
138
+ Raises:
139
+ BudgetExhaustedError: If any ceiling would be exceeded.
140
+ """
141
+ with self._lock:
142
+ self._check_token_ceilings(input_tokens, output_tokens, cost_micros_usd)
143
+ self._input_tokens_used += input_tokens
144
+ self._output_tokens_used += output_tokens
145
+ self._cost_micros_used += cost_micros_usd
146
+
147
+ def _check_token_ceilings(
148
+ self,
149
+ input_tokens: int,
150
+ output_tokens: int,
151
+ cost_micros_usd: int,
152
+ ) -> None:
153
+ """Check token/cost ceilings. Must be called while holding the lock."""
154
+ if (
155
+ self._budget.max_input_tokens is not None
156
+ and self._input_tokens_used + input_tokens > self._budget.max_input_tokens
157
+ ):
158
+ raise BudgetExhaustedError(
159
+ f"input token budget exhausted: "
160
+ f"{self._input_tokens_used + input_tokens} > {self._budget.max_input_tokens}"
161
+ )
162
+ if (
163
+ self._budget.max_output_tokens is not None
164
+ and self._output_tokens_used + output_tokens > self._budget.max_output_tokens
165
+ ):
166
+ raise BudgetExhaustedError(
167
+ f"output token budget exhausted: "
168
+ f"{self._output_tokens_used + output_tokens} > {self._budget.max_output_tokens}"
169
+ )
170
+ if (
171
+ self._budget.max_total_cost_micros_usd is not None
172
+ and self._cost_micros_used + cost_micros_usd > self._budget.max_total_cost_micros_usd
173
+ ):
174
+ raise BudgetExhaustedError(
175
+ f"cost budget exhausted: "
176
+ f"{self._cost_micros_used + cost_micros_usd} > "
177
+ f"{self._budget.max_total_cost_micros_usd} micro-USD"
178
+ )
179
+
180
+ async def async_reserve_tokens(
181
+ self,
182
+ *,
183
+ input_tokens: int = 0,
184
+ output_tokens: int = 0,
185
+ cost_micros_usd: int = 0,
186
+ ) -> None:
187
+ """Async-safe version of reserve_tokens using the shared thread lock.
188
+
189
+ Check-and-reserve is one asyncio.Lock-protected operation.
190
+
191
+ Args:
192
+ input_tokens: Input tokens to reserve.
193
+ output_tokens: Output tokens to reserve.
194
+ cost_micros_usd: Cost in integer micro-USD to reserve.
195
+
196
+ Raises:
197
+ BudgetExhaustedError: If any ceiling would be exceeded.
198
+ """
199
+ with self._lock:
200
+ self._check_token_ceilings(input_tokens, output_tokens, cost_micros_usd)
201
+ self._input_tokens_used += input_tokens
202
+ self._output_tokens_used += output_tokens
203
+ self._cost_micros_used += cost_micros_usd
204
+
205
+ def account_stream_delta(
206
+ self,
207
+ *,
208
+ input_tokens: int = 0,
209
+ output_tokens: int = 0,
210
+ cost_micros_usd: int = 0,
211
+ ) -> None:
212
+ """Update token/cost from a streaming LLM response delta.
213
+
214
+ Call this from the streaming consumer on each chunk. Raises
215
+ BudgetExhaustedError immediately if the incremental delta would exceed
216
+ the remaining reservation — aborting the stream early rather than waiting
217
+ for the full response.
218
+
219
+ Args:
220
+ input_tokens: Incremental input token delta.
221
+ output_tokens: Incremental output token delta.
222
+ cost_micros_usd: Incremental cost in integer micro-USD.
223
+
224
+ Raises:
225
+ BudgetExhaustedError: If any ceiling is exceeded mid-stream.
226
+ """
227
+ with self._lock:
228
+ self._check_token_ceilings(input_tokens, output_tokens, cost_micros_usd)
229
+ self._input_tokens_used += input_tokens
230
+ self._output_tokens_used += output_tokens
231
+ self._cost_micros_used += cost_micros_usd
232
+
233
+ def snapshot(self) -> dict[str, int | float | None]:
234
+ """Return a thread-safe snapshot of counters and ceilings.
235
+
236
+ Returns:
237
+ A plain dict of current usage counters, elapsed seconds, and budget
238
+ ceilings. Monetary values are integer micro-USD.
239
+ """
240
+ with self._lock:
241
+ elapsed = time.monotonic() - self._start_time
242
+ return {
243
+ "call_count": self._call_count,
244
+ "failure_count": self._failure_count,
245
+ "input_tokens_used": self._input_tokens_used,
246
+ "output_tokens_used": self._output_tokens_used,
247
+ "cost_micros_used": self._cost_micros_used,
248
+ "elapsed_seconds": elapsed,
249
+ "max_tool_calls": self._budget.max_tool_calls,
250
+ "failure_budget": self._budget.failure_budget,
251
+ "max_input_tokens": self._budget.max_input_tokens,
252
+ "max_output_tokens": self._budget.max_output_tokens,
253
+ "max_total_cost_micros_usd": self._budget.max_total_cost_micros_usd,
254
+ "max_seconds": self._budget.max_seconds,
255
+ }
256
+
257
+ @property
258
+ def call_count(self) -> int:
259
+ """Total tool call invocations recorded."""
260
+ with self._lock:
261
+ return self._call_count
262
+
263
+ @property
264
+ def failure_count(self) -> int:
265
+ """Total failed attempts recorded."""
266
+ with self._lock:
267
+ return self._failure_count
268
+
269
+ @property
270
+ def input_tokens_used(self) -> int:
271
+ """Total input tokens consumed."""
272
+ with self._lock:
273
+ return self._input_tokens_used
274
+
275
+ @property
276
+ def output_tokens_used(self) -> int:
277
+ """Total output tokens consumed."""
278
+ with self._lock:
279
+ return self._output_tokens_used
280
+
281
+ @property
282
+ def cost_micros_used(self) -> int:
283
+ """Total cost consumed in integer micro-USD."""
284
+ with self._lock:
285
+ return self._cost_micros_used
@@ -0,0 +1,24 @@
1
+ from agentic_exception_sdk.budget.models import AgentBudget
2
+
3
+ __all__ = ['BudgetWatchdog']
4
+
5
+ class BudgetWatchdog:
6
+ def __init__(self, budget: AgentBudget) -> None: ...
7
+ def consume_call(self) -> None: ...
8
+ def record_failure(self) -> None: ...
9
+ def check_time(self) -> None: ...
10
+ def check_before_sleep(self, sleep_seconds: float) -> None: ...
11
+ def reserve_tokens(self, *, input_tokens: int = 0, output_tokens: int = 0, cost_micros_usd: int = 0) -> None: ...
12
+ async def async_reserve_tokens(self, *, input_tokens: int = 0, output_tokens: int = 0, cost_micros_usd: int = 0) -> None: ...
13
+ def account_stream_delta(self, *, input_tokens: int = 0, output_tokens: int = 0, cost_micros_usd: int = 0) -> None: ...
14
+ def snapshot(self) -> dict[str, int | float | None]: ...
15
+ @property
16
+ def call_count(self) -> int: ...
17
+ @property
18
+ def failure_count(self) -> int: ...
19
+ @property
20
+ def input_tokens_used(self) -> int: ...
21
+ @property
22
+ def output_tokens_used(self) -> int: ...
23
+ @property
24
+ def cost_micros_used(self) -> int: ...
@@ -0,0 +1,102 @@
1
+ """ResilienceBundle — injectable container for all SDK components.
2
+
3
+ All fields default to safe NoOp implementations so a bundle with defaults is
4
+ immediately usable without any configuration. Host projects replace individual
5
+ components via constructor injection — never via global state or monkey-patching.
6
+
7
+ Zero host imports: this module only imports from within the SDK.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from agentic_exception_sdk.budget.models import AgentBudget
16
+ from agentic_exception_sdk.budget.watchdog import BudgetWatchdog
17
+ from agentic_exception_sdk.escalation.router import NoOpEscalationRouter
18
+ from agentic_exception_sdk.observability.metrics import (
19
+ MetricsCollector,
20
+ NoOpMetricsCollector,
21
+ )
22
+ from agentic_exception_sdk.observability.sink import NoOpSink
23
+ from agentic_exception_sdk.propagation.bus import InMemoryBus
24
+ from agentic_exception_sdk.propagation.dlq import InMemoryDLQ
25
+ from agentic_exception_sdk.resilience.circuit_breaker import (
26
+ NoOpCircuitBreaker,
27
+ )
28
+ from agentic_exception_sdk.resilience.retry import NoOpRetry
29
+ from agentic_exception_sdk.taxonomy.classifier import ExceptionClassifier
30
+ from agentic_exception_sdk.validation.gates import NoOpGate
31
+ from agentic_exception_sdk.validation.guard_rails import NoOpGuardRails
32
+ from agentic_exception_sdk.validation.trust_boundary import TrustBoundaryValidator
33
+
34
+ if TYPE_CHECKING:
35
+ from opentelemetry.metrics import MeterProvider
36
+
37
+ __all__ = ["ResilienceBundle"]
38
+
39
+
40
+ @dataclass
41
+ class ResilienceBundle:
42
+ """All SDK components in one injectable container.
43
+
44
+ Defaults every component to a safe no-op implementation. Host projects
45
+ replace individual fields to enable specific capabilities:
46
+
47
+ bundle = ResilienceBundle(
48
+ circuit_breaker=InMemoryCircuitBreaker(failure_threshold=3),
49
+ exception_sink=OTelExceptionAdapter(tracer_provider),
50
+ )
51
+
52
+ Attributes:
53
+ classifier: Maps exceptions to (class, source, level) tuples.
54
+ trust_boundary: Sanitizes all host-provided strings and snapshots.
55
+ guard_rails: Enforces tool call allowlist. NoOpGuardRails by default.
56
+ agent_budget: Tracks and enforces resource budgets. Unlimited by default.
57
+ retry_policy: Retry strategy. Single-attempt (no retry) by default.
58
+ circuit_breaker: Sync circuit breaker. NoOp (pass-through) by default.
59
+ output_validation_gate: Validates tool outputs. Pass-through by default.
60
+ exception_sink: Observability sink. Discards all envelopes by default.
61
+ metrics_collector: Metrics collector. NoOp by default.
62
+ meter_provider: Optional OTel MeterProvider for tool latency histograms.
63
+ propagation_bus: Propagation bus. Bounded in-memory (1000 envelopes) by default.
64
+ dlq: Dead-letter queue for HARD_KILL envelopes. InMemoryDLQ by default.
65
+ escalation_router: Routes envelopes to handlers. NoOp by default.
66
+ recovery_policy: Optional post-classification recovery policy. None by default.
67
+ async_circuit_breaker: Optional async circuit breaker. None by default,
68
+ meaning async_resilient() falls back to bundle.circuit_breaker.
69
+ """
70
+
71
+ classifier: ExceptionClassifier = field(default_factory=ExceptionClassifier)
72
+ trust_boundary: TrustBoundaryValidator = field(
73
+ default_factory=TrustBoundaryValidator
74
+ )
75
+ guard_rails: Any = field(default_factory=NoOpGuardRails)
76
+ agent_budget: BudgetWatchdog = field(
77
+ default_factory=lambda: BudgetWatchdog(AgentBudget())
78
+ )
79
+ retry_policy: Any = field(default_factory=NoOpRetry)
80
+ circuit_breaker: Any = field(default_factory=NoOpCircuitBreaker)
81
+ output_validation_gate: Any = field(default_factory=NoOpGate)
82
+ exception_sink: Any = field(default_factory=NoOpSink)
83
+ metrics_collector: MetricsCollector = field(default_factory=NoOpMetricsCollector)
84
+ meter_provider: MeterProvider | None = None
85
+ propagation_bus: Any = field(default_factory=InMemoryBus)
86
+ dlq: Any = field(default_factory=InMemoryDLQ)
87
+ escalation_router: Any = field(default_factory=NoOpEscalationRouter)
88
+ recovery_policy: Any | None = None
89
+ async_circuit_breaker: Any | None = None
90
+ _latency_histogram: Any | None = field(init=False, repr=False, default=None)
91
+
92
+ def __post_init__(self) -> None:
93
+ """Cache optional OTel instruments once at bundle construction."""
94
+ if self.meter_provider is None:
95
+ self._latency_histogram = None
96
+ return
97
+ meter = self.meter_provider.get_meter("agentic_exception_sdk")
98
+ self._latency_histogram = meter.create_histogram(
99
+ "agent_tool_call_duration_seconds",
100
+ unit="s",
101
+ description="Per-call tool duration by agent and tool name",
102
+ )
@@ -0,0 +1,37 @@
1
+ from _typeshed import Incomplete
2
+ from agentic_exception_sdk.budget.watchdog import BudgetWatchdog
3
+ from agentic_exception_sdk.escalation.router import NoOpEscalationRouter
4
+ from agentic_exception_sdk.observability.metrics import MetricsCollector, NoOpMetricsCollector
5
+ from agentic_exception_sdk.observability.sink import NoOpSink
6
+ from agentic_exception_sdk.propagation.bus import InMemoryBus
7
+ from agentic_exception_sdk.propagation.dlq import InMemoryDLQ
8
+ from agentic_exception_sdk.resilience.circuit_breaker import NoOpCircuitBreaker
9
+ from agentic_exception_sdk.resilience.retry import NoOpRetry
10
+ from agentic_exception_sdk.taxonomy.classifier import ExceptionClassifier
11
+ from agentic_exception_sdk.validation.gates import NoOpGate
12
+ from agentic_exception_sdk.validation.guard_rails import NoOpGuardRails
13
+ from agentic_exception_sdk.validation.trust_boundary import TrustBoundaryValidator
14
+ from dataclasses import dataclass, field
15
+ from opentelemetry.metrics import MeterProvider
16
+ from typing import Any
17
+
18
+ __all__ = ['ResilienceBundle']
19
+
20
+ @dataclass
21
+ class ResilienceBundle:
22
+ classifier: ExceptionClassifier = field(default_factory=ExceptionClassifier)
23
+ trust_boundary: TrustBoundaryValidator = field(default_factory=TrustBoundaryValidator)
24
+ guard_rails: Any = field(default_factory=NoOpGuardRails)
25
+ agent_budget: BudgetWatchdog = field(default_factory=Incomplete)
26
+ retry_policy: Any = field(default_factory=NoOpRetry)
27
+ circuit_breaker: Any = field(default_factory=NoOpCircuitBreaker)
28
+ output_validation_gate: Any = field(default_factory=NoOpGate)
29
+ exception_sink: Any = field(default_factory=NoOpSink)
30
+ metrics_collector: MetricsCollector = field(default_factory=NoOpMetricsCollector)
31
+ meter_provider: MeterProvider | None = ...
32
+ propagation_bus: Any = field(default_factory=InMemoryBus)
33
+ dlq: Any = field(default_factory=InMemoryDLQ)
34
+ escalation_router: Any = field(default_factory=NoOpEscalationRouter)
35
+ recovery_policy: Any | None = ...
36
+ async_circuit_breaker: Any | None = ...
37
+ def __post_init__(self) -> None: ...
@@ -0,0 +1,10 @@
1
+ """Startup configuration validation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agentic_exception_sdk.config.validate import BundleValidationError, validate_bundle
6
+
7
+ __all__ = [
8
+ "BundleValidationError",
9
+ "validate_bundle",
10
+ ]
@@ -0,0 +1,3 @@
1
+ from agentic_exception_sdk.config.validate import BundleValidationError as BundleValidationError, validate_bundle as validate_bundle
2
+
3
+ __all__ = ['BundleValidationError', 'validate_bundle']
@@ -0,0 +1,74 @@
1
+ """Startup validation for ResilienceBundle configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agentic_exception_sdk.bundle import ResilienceBundle
6
+
7
+ __all__ = [
8
+ "BundleValidationError",
9
+ "validate_bundle",
10
+ ]
11
+
12
+
13
+ class BundleValidationError(ValueError):
14
+ """Raised when validate_bundle finds one or more configuration failures."""
15
+
16
+ def __init__(self, failures: list[str]) -> None:
17
+ self.failures = list(failures)
18
+ self.report = "\n".join(self.failures)
19
+ super().__init__(self.report)
20
+
21
+
22
+ def validate_bundle(bundle: ResilienceBundle) -> None:
23
+ """Validate a bundle at startup without mutating it.
24
+
25
+ Args:
26
+ bundle: The configured ResilienceBundle to inspect.
27
+
28
+ Raises:
29
+ BundleValidationError: If one or more configuration failures are found.
30
+ """
31
+ failures: list[str] = []
32
+ _check_redis_circuit_breaker(bundle, failures)
33
+ _check_guard_rails(bundle, failures)
34
+ _check_budget(bundle, failures)
35
+ if failures:
36
+ raise BundleValidationError(failures)
37
+
38
+
39
+ def _check_redis_circuit_breaker(
40
+ bundle: ResilienceBundle,
41
+ failures: list[str],
42
+ ) -> None:
43
+ redis_url = getattr(bundle.circuit_breaker, "redis_url", None)
44
+ if redis_url is None:
45
+ return
46
+ if not isinstance(redis_url, str) or not redis_url.startswith("rediss://"):
47
+ failures.append("RedisCircuitBreaker: URL must use rediss:// (TLS required)")
48
+
49
+
50
+ def _check_guard_rails(bundle: ResilienceBundle, failures: list[str]) -> None:
51
+ guard_rails = bundle.guard_rails
52
+ allowlist = getattr(guard_rails, "allowlist", None)
53
+ if allowlist is not None and len(allowlist) == 0:
54
+ failures.append("GuardRailPolicy: allowlist is empty - all tool calls will be blocked")
55
+ return
56
+
57
+ allowlisted_operations = getattr(guard_rails, "_allowlisted_operations", None)
58
+ canonical = getattr(allowlisted_operations, "canonical", None)
59
+ if canonical is not None and len(canonical) == 0:
60
+ failures.append("GuardRailPolicy: allowlist is empty - all tool calls will be blocked")
61
+
62
+
63
+ def _check_budget(bundle: ResilienceBundle, failures: list[str]) -> None:
64
+ budget = getattr(bundle.agent_budget, "budget", None)
65
+ if budget is None:
66
+ budget = getattr(bundle.agent_budget, "_budget", None)
67
+ if budget is None:
68
+ return
69
+
70
+ max_cost = getattr(budget, "max_cost_micros", None)
71
+ if max_cost is None:
72
+ max_cost = getattr(budget, "max_total_cost_micros_usd", None)
73
+ if max_cost is not None and max_cost <= 0:
74
+ failures.append("AgentBudget: max_cost_micros must be positive")
@@ -0,0 +1,11 @@
1
+ from _typeshed import Incomplete
2
+ from agentic_exception_sdk.bundle import ResilienceBundle
3
+
4
+ __all__ = ['BundleValidationError', 'validate_bundle']
5
+
6
+ class BundleValidationError(ValueError):
7
+ failures: Incomplete
8
+ report: Incomplete
9
+ def __init__(self, failures: list[str]) -> None: ...
10
+
11
+ def validate_bundle(bundle: ResilienceBundle) -> None: ...
@@ -0,0 +1,22 @@
1
+ """Escalation: routing, handlers, and checkpoint store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agentic_exception_sdk.escalation.checkpoint import CheckpointStore, InMemoryCheckpointStore
6
+ from agentic_exception_sdk.escalation.handlers import NoOpHandler
7
+ from agentic_exception_sdk.escalation.router import (
8
+ EscalationHandlerLike,
9
+ EscalationRouter,
10
+ NoOpEscalationRouter,
11
+ RecoveryDirective,
12
+ )
13
+
14
+ __all__ = [
15
+ "CheckpointStore",
16
+ "EscalationHandlerLike",
17
+ "EscalationRouter",
18
+ "InMemoryCheckpointStore",
19
+ "NoOpEscalationRouter",
20
+ "NoOpHandler",
21
+ "RecoveryDirective",
22
+ ]
@@ -0,0 +1,5 @@
1
+ from agentic_exception_sdk.escalation.checkpoint import CheckpointStore as CheckpointStore, InMemoryCheckpointStore as InMemoryCheckpointStore
2
+ from agentic_exception_sdk.escalation.handlers import NoOpHandler as NoOpHandler
3
+ from agentic_exception_sdk.escalation.router import EscalationHandlerLike as EscalationHandlerLike, EscalationRouter as EscalationRouter, NoOpEscalationRouter as NoOpEscalationRouter, RecoveryDirective as RecoveryDirective
4
+
5
+ __all__ = ['CheckpointStore', 'EscalationHandlerLike', 'EscalationRouter', 'InMemoryCheckpointStore', 'NoOpEscalationRouter', 'NoOpHandler', 'RecoveryDirective']