agent-killswitch 0.1.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.
- agent_killswitch/__init__.py +76 -0
- agent_killswitch/_version.py +5 -0
- agent_killswitch/backends/__init__.py +21 -0
- agent_killswitch/backends/base.py +95 -0
- agent_killswitch/backends/memory.py +120 -0
- agent_killswitch/backends/redis.py +230 -0
- agent_killswitch/core/__init__.py +36 -0
- agent_killswitch/core/budget_kill.py +281 -0
- agent_killswitch/core/cascading.py +280 -0
- agent_killswitch/core/circuit_breaker.py +323 -0
- agent_killswitch/core/enums.py +112 -0
- agent_killswitch/core/heartbeat.py +313 -0
- agent_killswitch/core/killswitch.py +581 -0
- agent_killswitch/core/models.py +173 -0
- agent_killswitch/decorators.py +302 -0
- agent_killswitch/integrations/__init__.py +17 -0
- agent_killswitch/integrations/asyncio_tasks.py +211 -0
- agent_killswitch/integrations/crewai.py +151 -0
- agent_killswitch/integrations/langchain.py +135 -0
- agent_killswitch/integrations/langgraph.py +143 -0
- agent_killswitch/integrations/openai_agents.py +151 -0
- agent_killswitch/py.typed +0 -0
- agent_killswitch-0.1.0.dist-info/METADATA +392 -0
- agent_killswitch-0.1.0.dist-info/RECORD +26 -0
- agent_killswitch-0.1.0.dist-info/WHEEL +4 -0
- agent_killswitch-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""agent-killswitch: Operational safety controls for autonomous AI agents.
|
|
2
|
+
|
|
3
|
+
Zero-dependency kill switch, heartbeat monitor, circuit breaker, and
|
|
4
|
+
budget controls for AI agents. Works with any framework (LangChain,
|
|
5
|
+
LangGraph, CrewAI, OpenAI Agents SDK, asyncio) or standalone.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from agent_killswitch import KillSwitch, KillLevel, KillScope
|
|
10
|
+
|
|
11
|
+
ks = KillSwitch()
|
|
12
|
+
ks.activate(KillLevel.KILL, KillScope.GLOBAL, reason="emergency")
|
|
13
|
+
status = ks.check()
|
|
14
|
+
assert status.is_killed
|
|
15
|
+
|
|
16
|
+
Core classes:
|
|
17
|
+
KillSwitch: Main kill switch with tiered levels and scoped targets.
|
|
18
|
+
HeartbeatMonitor: Track agent health via periodic heartbeats.
|
|
19
|
+
CircuitBreaker: Protect against cascading failures.
|
|
20
|
+
BudgetKillTrigger: Kill agents that exceed their budget.
|
|
21
|
+
CascadingTerminator: Propagate kills through agent hierarchies.
|
|
22
|
+
|
|
23
|
+
Decorators:
|
|
24
|
+
killswitch_protected: Check kill switch before/after function calls.
|
|
25
|
+
with_heartbeat: Send heartbeats during long-running operations.
|
|
26
|
+
circuit_breaker: Wrap functions with circuit breaker protection.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from ._version import __version__
|
|
32
|
+
from .core.budget_kill import BudgetKillTrigger
|
|
33
|
+
from .core.cascading import CascadingTerminator
|
|
34
|
+
from .core.circuit_breaker import CircuitBreaker, CircuitBreakerOpen
|
|
35
|
+
from .core.enums import CircuitState, HeartbeatStatus, KillLevel, KillScope
|
|
36
|
+
from .core.heartbeat import HeartbeatMonitor
|
|
37
|
+
from .core.killswitch import KillSwitch
|
|
38
|
+
from .core.models import (
|
|
39
|
+
AgentRegistration,
|
|
40
|
+
BudgetStatus,
|
|
41
|
+
CircuitBreakerState,
|
|
42
|
+
HeartbeatRecord,
|
|
43
|
+
KillEvent,
|
|
44
|
+
KillStatus,
|
|
45
|
+
)
|
|
46
|
+
from .decorators import KillSwitchTriggered, killswitch_protected, with_heartbeat
|
|
47
|
+
from .decorators import circuit_breaker as circuit_breaker_decorator
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"AgentRegistration",
|
|
51
|
+
"BudgetKillTrigger",
|
|
52
|
+
"BudgetStatus",
|
|
53
|
+
"CascadingTerminator",
|
|
54
|
+
"CircuitBreaker",
|
|
55
|
+
"CircuitBreakerOpen",
|
|
56
|
+
"CircuitBreakerState",
|
|
57
|
+
"CircuitState",
|
|
58
|
+
"HeartbeatMonitor",
|
|
59
|
+
"HeartbeatRecord",
|
|
60
|
+
"HeartbeatStatus",
|
|
61
|
+
# Models
|
|
62
|
+
"KillEvent",
|
|
63
|
+
# Enums
|
|
64
|
+
"KillLevel",
|
|
65
|
+
"KillScope",
|
|
66
|
+
"KillStatus",
|
|
67
|
+
# Core classes
|
|
68
|
+
"KillSwitch",
|
|
69
|
+
"KillSwitchTriggered",
|
|
70
|
+
# Version
|
|
71
|
+
"__version__",
|
|
72
|
+
"circuit_breaker_decorator",
|
|
73
|
+
# Decorators
|
|
74
|
+
"killswitch_protected",
|
|
75
|
+
"with_heartbeat",
|
|
76
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Backend implementations for kill switch state storage.
|
|
2
|
+
|
|
3
|
+
Available backends:
|
|
4
|
+
- InMemoryBackend: Zero-dependency, thread-safe, dict-based (default)
|
|
5
|
+
- RedisBackend: Distributed, Redis-backed (optional dependency)
|
|
6
|
+
- KillSwitchBackend: Protocol for custom backends
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .base import KillSwitchBackend
|
|
12
|
+
from .memory import InMemoryBackend
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"InMemoryBackend",
|
|
16
|
+
"KillSwitchBackend",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# RedisBackend is intentionally not imported here to avoid
|
|
20
|
+
# requiring the redis dependency. Import it directly:
|
|
21
|
+
# from agent_killswitch.backends.redis import RedisBackend
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Abstract backend protocol for kill switch state storage.
|
|
2
|
+
|
|
3
|
+
Backends provide the storage layer for kill switch state. The protocol
|
|
4
|
+
defines the minimal interface that any backend must implement.
|
|
5
|
+
|
|
6
|
+
Two implementations are provided:
|
|
7
|
+
- InMemoryBackend: Zero-dependency, dict-based (default)
|
|
8
|
+
- RedisBackend: Distributed, Redis-backed (optional)
|
|
9
|
+
|
|
10
|
+
Custom backends can be created by implementing this protocol.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class KillSwitchBackend(Protocol):
|
|
20
|
+
"""Protocol for kill switch state backends.
|
|
21
|
+
|
|
22
|
+
All methods are synchronous. For async usage, wrap in an executor
|
|
23
|
+
or use an async-native backend.
|
|
24
|
+
|
|
25
|
+
The backend MUST be fail-closed: if any operation fails, the caller
|
|
26
|
+
should treat the agent as KILLED for safety.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get(self, key: str) -> str | None:
|
|
30
|
+
"""Get a value by key.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
key: The storage key.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The stored value as a string, or None if not found.
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def set(self, key: str, value: str) -> None:
|
|
41
|
+
"""Set a value by key. No TTL -- explicit deletion required.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
key: The storage key.
|
|
45
|
+
value: The value to store.
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def delete(self, key: str) -> None:
|
|
50
|
+
"""Delete a key.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
key: The storage key to delete.
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def get_many(self, keys: list[str]) -> list[str | None]:
|
|
58
|
+
"""Get multiple values in a single operation.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
keys: List of storage keys.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of values (or None) in the same order as keys.
|
|
65
|
+
"""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def keys(self, pattern: str) -> list[str]:
|
|
69
|
+
"""Find keys matching a pattern.
|
|
70
|
+
|
|
71
|
+
The pattern uses simple prefix matching (not glob).
|
|
72
|
+
Pass "prefix*" to match all keys starting with "prefix".
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
pattern: Key pattern to match (prefix with trailing *).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of matching keys.
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def exists(self, key: str) -> bool:
|
|
83
|
+
"""Check if a key exists.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
key: The storage key.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the key exists.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def clear(self) -> None:
|
|
94
|
+
"""Remove all keys. Use with caution -- primarily for testing."""
|
|
95
|
+
...
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""In-memory backend for kill switch state storage.
|
|
2
|
+
|
|
3
|
+
Zero-dependency, thread-safe, dict-based backend. This is the default
|
|
4
|
+
backend and is suitable for:
|
|
5
|
+
- Single-process applications
|
|
6
|
+
- Testing and development
|
|
7
|
+
- Applications that don't need distributed state
|
|
8
|
+
|
|
9
|
+
For distributed systems, use the RedisBackend instead.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InMemoryBackend:
|
|
18
|
+
"""Thread-safe in-memory backend using a dictionary.
|
|
19
|
+
|
|
20
|
+
All operations are protected by a reentrant lock for thread safety.
|
|
21
|
+
This backend is the default when no external backend is configured.
|
|
22
|
+
|
|
23
|
+
Example::
|
|
24
|
+
|
|
25
|
+
from agent_killswitch.backends.memory import InMemoryBackend
|
|
26
|
+
|
|
27
|
+
backend = InMemoryBackend()
|
|
28
|
+
backend.set("killswitch:global", "kill")
|
|
29
|
+
assert backend.get("killswitch:global") == "kill"
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
"""Initialize the in-memory backend."""
|
|
34
|
+
self._store: dict[str, str] = {}
|
|
35
|
+
self._lock = threading.RLock()
|
|
36
|
+
|
|
37
|
+
def get(self, key: str) -> str | None:
|
|
38
|
+
"""Get a value by key.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
key: The storage key.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The stored value, or None if not found.
|
|
45
|
+
"""
|
|
46
|
+
with self._lock:
|
|
47
|
+
return self._store.get(key)
|
|
48
|
+
|
|
49
|
+
def set(self, key: str, value: str) -> None:
|
|
50
|
+
"""Set a value by key.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
key: The storage key.
|
|
54
|
+
value: The value to store.
|
|
55
|
+
"""
|
|
56
|
+
with self._lock:
|
|
57
|
+
self._store[key] = value
|
|
58
|
+
|
|
59
|
+
def delete(self, key: str) -> None:
|
|
60
|
+
"""Delete a key.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
key: The storage key to delete.
|
|
64
|
+
"""
|
|
65
|
+
with self._lock:
|
|
66
|
+
self._store.pop(key, None)
|
|
67
|
+
|
|
68
|
+
def get_many(self, keys: list[str]) -> list[str | None]:
|
|
69
|
+
"""Get multiple values atomically.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
keys: List of storage keys.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of values in the same order as keys.
|
|
76
|
+
"""
|
|
77
|
+
with self._lock:
|
|
78
|
+
return [self._store.get(k) for k in keys]
|
|
79
|
+
|
|
80
|
+
def keys(self, pattern: str) -> list[str]:
|
|
81
|
+
"""Find keys matching a prefix pattern.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
pattern: Key pattern. If it ends with '*', matches prefix.
|
|
85
|
+
Otherwise, matches exact key.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of matching keys.
|
|
89
|
+
"""
|
|
90
|
+
with self._lock:
|
|
91
|
+
if pattern.endswith("*"):
|
|
92
|
+
prefix = pattern[:-1]
|
|
93
|
+
return [k for k in self._store if k.startswith(prefix)]
|
|
94
|
+
return [k for k in self._store if k == pattern]
|
|
95
|
+
|
|
96
|
+
def exists(self, key: str) -> bool:
|
|
97
|
+
"""Check if a key exists.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
key: The storage key.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if the key exists.
|
|
104
|
+
"""
|
|
105
|
+
with self._lock:
|
|
106
|
+
return key in self._store
|
|
107
|
+
|
|
108
|
+
def clear(self) -> None:
|
|
109
|
+
"""Remove all keys."""
|
|
110
|
+
with self._lock:
|
|
111
|
+
self._store.clear()
|
|
112
|
+
|
|
113
|
+
def __len__(self) -> int:
|
|
114
|
+
"""Return the number of stored keys."""
|
|
115
|
+
with self._lock:
|
|
116
|
+
return len(self._store)
|
|
117
|
+
|
|
118
|
+
def __repr__(self) -> str:
|
|
119
|
+
with self._lock:
|
|
120
|
+
return f"InMemoryBackend(keys={len(self._store)})"
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Redis backend for distributed kill switch state storage.
|
|
2
|
+
|
|
3
|
+
Requires the `redis` optional dependency:
|
|
4
|
+
pip install agent-killswitch[redis]
|
|
5
|
+
|
|
6
|
+
This backend is suitable for:
|
|
7
|
+
- Multi-process applications
|
|
8
|
+
- Distributed systems
|
|
9
|
+
- Production deployments where multiple services need shared kill state
|
|
10
|
+
|
|
11
|
+
The backend uses Redis pipelines for efficient multi-key operations
|
|
12
|
+
and falls back to fail-closed behavior on connection errors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RedisBackend:
|
|
24
|
+
"""Redis-backed storage for kill switch state.
|
|
25
|
+
|
|
26
|
+
Uses Redis for sub-millisecond flag checks across distributed systems.
|
|
27
|
+
All keys use a configurable prefix (default: "killswitch:") to avoid
|
|
28
|
+
collisions with other Redis users.
|
|
29
|
+
|
|
30
|
+
Fail-closed behavior: If Redis is unreachable, operations raise
|
|
31
|
+
exceptions. The KillSwitch class interprets backend failures as
|
|
32
|
+
KILLED state for safety.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
import redis
|
|
37
|
+
from agent_killswitch.backends.redis import RedisBackend
|
|
38
|
+
|
|
39
|
+
client = redis.Redis(host="localhost", port=6379, db=0)
|
|
40
|
+
backend = RedisBackend(client, prefix="myapp:killswitch:")
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
redis_client: A redis.Redis or redis.StrictRedis instance.
|
|
44
|
+
prefix: Key prefix for all kill switch keys.
|
|
45
|
+
Defaults to "killswitch:".
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
redis_client: Any,
|
|
51
|
+
prefix: str = "killswitch:",
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize the Redis backend.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
redis_client: A Redis client instance. Must support get, set,
|
|
57
|
+
delete, pipeline, scan_iter, and exists methods.
|
|
58
|
+
prefix: Key prefix for namespacing. Defaults to "killswitch:".
|
|
59
|
+
"""
|
|
60
|
+
self._redis = redis_client
|
|
61
|
+
self._prefix = prefix
|
|
62
|
+
|
|
63
|
+
def _prefixed(self, key: str) -> str:
|
|
64
|
+
"""Add prefix to a key if not already prefixed.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
key: The raw key.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The prefixed key.
|
|
71
|
+
"""
|
|
72
|
+
if key.startswith(self._prefix):
|
|
73
|
+
return key
|
|
74
|
+
return f"{self._prefix}{key}"
|
|
75
|
+
|
|
76
|
+
def _unprefixed(self, key: str) -> str:
|
|
77
|
+
"""Remove prefix from a key.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
key: The prefixed key.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The raw key without prefix.
|
|
84
|
+
"""
|
|
85
|
+
if key.startswith(self._prefix):
|
|
86
|
+
return key[len(self._prefix) :]
|
|
87
|
+
return key
|
|
88
|
+
|
|
89
|
+
def get(self, key: str) -> str | None:
|
|
90
|
+
"""Get a value from Redis.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
key: The storage key.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The stored value as a string, or None if not found.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
Exception: On Redis connection failure (fail-closed).
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
val = self._redis.get(self._prefixed(key))
|
|
103
|
+
if val is not None and isinstance(val, bytes):
|
|
104
|
+
return val.decode("utf-8")
|
|
105
|
+
return val
|
|
106
|
+
except Exception:
|
|
107
|
+
logger.error("Redis GET failed for key=%s", key, exc_info=True)
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def set(self, key: str, value: str) -> None:
|
|
111
|
+
"""Set a value in Redis. No TTL -- explicit deletion required.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
key: The storage key.
|
|
115
|
+
value: The value to store.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
Exception: On Redis connection failure.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
self._redis.set(self._prefixed(key), value)
|
|
122
|
+
except Exception:
|
|
123
|
+
logger.error("Redis SET failed for key=%s", key, exc_info=True)
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
def delete(self, key: str) -> None:
|
|
127
|
+
"""Delete a key from Redis.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
key: The storage key to delete.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
Exception: On Redis connection failure.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
self._redis.delete(self._prefixed(key))
|
|
137
|
+
except Exception:
|
|
138
|
+
logger.error("Redis DELETE failed for key=%s", key, exc_info=True)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
def get_many(self, keys: list[str]) -> list[str | None]:
|
|
142
|
+
"""Get multiple values using a Redis pipeline (single round-trip).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
keys: List of storage keys.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of values (or None) in the same order as keys.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
Exception: On Redis connection failure.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
pipe = self._redis.pipeline(transaction=False)
|
|
155
|
+
for key in keys:
|
|
156
|
+
pipe.get(self._prefixed(key))
|
|
157
|
+
results = pipe.execute()
|
|
158
|
+
return [r.decode("utf-8") if isinstance(r, bytes) else r for r in results]
|
|
159
|
+
except Exception:
|
|
160
|
+
logger.error("Redis pipeline GET failed for keys=%s", keys, exc_info=True)
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
def keys(self, pattern: str) -> list[str]:
|
|
164
|
+
"""Find keys matching a pattern using Redis SCAN.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
pattern: Key pattern. If it ends with '*', uses SCAN with
|
|
168
|
+
the prefixed pattern. Otherwise matches exact key.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of matching keys (without prefix).
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
Exception: On Redis connection failure.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
if pattern.endswith("*"):
|
|
178
|
+
redis_pattern = self._prefixed(pattern)
|
|
179
|
+
found: list[str] = []
|
|
180
|
+
for key in self._redis.scan_iter(match=redis_pattern):
|
|
181
|
+
if isinstance(key, bytes):
|
|
182
|
+
key = key.decode("utf-8")
|
|
183
|
+
found.append(self._unprefixed(key))
|
|
184
|
+
return found
|
|
185
|
+
else:
|
|
186
|
+
prefixed = self._prefixed(pattern)
|
|
187
|
+
if self._redis.exists(prefixed):
|
|
188
|
+
return [pattern]
|
|
189
|
+
return []
|
|
190
|
+
except Exception:
|
|
191
|
+
logger.error("Redis KEYS failed for pattern=%s", pattern, exc_info=True)
|
|
192
|
+
raise
|
|
193
|
+
|
|
194
|
+
def exists(self, key: str) -> bool:
|
|
195
|
+
"""Check if a key exists in Redis.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
key: The storage key.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if the key exists.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
Exception: On Redis connection failure.
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
return bool(self._redis.exists(self._prefixed(key)))
|
|
208
|
+
except Exception:
|
|
209
|
+
logger.error("Redis EXISTS failed for key=%s", key, exc_info=True)
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
def clear(self) -> None:
|
|
213
|
+
"""Remove all keys with our prefix. Use with caution.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
Exception: On Redis connection failure.
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
pattern = f"{self._prefix}*"
|
|
220
|
+
keys_to_delete: list[Any] = []
|
|
221
|
+
for key in self._redis.scan_iter(match=pattern):
|
|
222
|
+
keys_to_delete.append(key)
|
|
223
|
+
if keys_to_delete:
|
|
224
|
+
self._redis.delete(*keys_to_delete)
|
|
225
|
+
except Exception:
|
|
226
|
+
logger.error("Redis CLEAR failed", exc_info=True)
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
def __repr__(self) -> str:
|
|
230
|
+
return f"RedisBackend(prefix={self._prefix!r})"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Core kill switch components.
|
|
2
|
+
|
|
3
|
+
This package contains the core functionality:
|
|
4
|
+
- killswitch: Main KillSwitch class (unified API)
|
|
5
|
+
- heartbeat: HeartbeatMonitor for agent health tracking
|
|
6
|
+
- circuit_breaker: Circuit breaker pattern
|
|
7
|
+
- budget_kill: Budget-based kill triggers
|
|
8
|
+
- cascading: Cascading termination for agent hierarchies
|
|
9
|
+
- models: Data models
|
|
10
|
+
- enums: Enumerations
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .enums import CircuitState, HeartbeatStatus, KillLevel, KillScope
|
|
16
|
+
from .models import (
|
|
17
|
+
AgentRegistration,
|
|
18
|
+
BudgetStatus,
|
|
19
|
+
CircuitBreakerState,
|
|
20
|
+
HeartbeatRecord,
|
|
21
|
+
KillEvent,
|
|
22
|
+
KillStatus,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AgentRegistration",
|
|
27
|
+
"BudgetStatus",
|
|
28
|
+
"CircuitBreakerState",
|
|
29
|
+
"CircuitState",
|
|
30
|
+
"HeartbeatRecord",
|
|
31
|
+
"HeartbeatStatus",
|
|
32
|
+
"KillEvent",
|
|
33
|
+
"KillLevel",
|
|
34
|
+
"KillScope",
|
|
35
|
+
"KillStatus",
|
|
36
|
+
]
|