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.
@@ -0,0 +1,151 @@
1
+ """CrewAI integration for agent-killswitch.
2
+
3
+ Provides a pre-action hook that checks the kill switch before
4
+ each CrewAI agent action.
5
+
6
+ Requires: pip install agent-killswitch[crewai]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from collections.abc import Callable
13
+ from typing import Any
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class KillSwitchHook:
19
+ """CrewAI pre-action hook that checks the kill switch.
20
+
21
+ This hook is designed to be called before each agent action in
22
+ a CrewAI workflow. If the kill switch is active, it prevents
23
+ the action from executing.
24
+
25
+ Example::
26
+
27
+ from agent_killswitch import KillSwitch
28
+ from agent_killswitch.integrations.crewai import KillSwitchHook
29
+
30
+ ks = KillSwitch()
31
+ hook = KillSwitchHook(killswitch=ks, crew_id="my-crew")
32
+
33
+ # In your CrewAI setup:
34
+ # Use hook.before_action as a step callback
35
+ if hook.before_action(agent_id="researcher"):
36
+ # Action is allowed
37
+ pass
38
+
39
+ Args:
40
+ killswitch: The KillSwitch instance.
41
+ crew_id: The crew ID for scope checks.
42
+ on_blocked: Callback when an action is blocked by kill switch.
43
+ Signature: (agent_id: str, level: str, reason: str) -> None
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ killswitch: Any,
49
+ crew_id: str | None = None,
50
+ on_blocked: Callable[[str, str, str], None] | None = None,
51
+ ) -> None:
52
+ self._ks = killswitch
53
+ self._crew_id = crew_id
54
+ self._on_blocked = on_blocked
55
+
56
+ def before_action(self, agent_id: str | None = None) -> bool:
57
+ """Check if the agent action should proceed.
58
+
59
+ Args:
60
+ agent_id: The agent attempting the action.
61
+
62
+ Returns:
63
+ True if the action should proceed, False if blocked.
64
+ """
65
+ status = self._ks.check(
66
+ scope_target=agent_id,
67
+ crew_id=self._crew_id,
68
+ )
69
+
70
+ if status.is_killed:
71
+ level_str = status.level.value if status.level else "unknown"
72
+ reason = status.reason or ""
73
+
74
+ logger.warning(
75
+ "crewai_action_blocked",
76
+ extra={
77
+ "agent_id": agent_id,
78
+ "crew_id": self._crew_id,
79
+ "level": level_str,
80
+ "reason": reason,
81
+ },
82
+ )
83
+
84
+ if self._on_blocked is not None:
85
+ try:
86
+ self._on_blocked(agent_id or "", level_str, reason)
87
+ except Exception as exc:
88
+ logger.error(
89
+ "on_blocked callback error",
90
+ extra={"error": str(exc)},
91
+ )
92
+
93
+ return False
94
+
95
+ return True
96
+
97
+ def create_step_callback(
98
+ self,
99
+ agent_id: str | None = None,
100
+ ) -> Callable[[Any], Any]:
101
+ """Create a CrewAI step callback function.
102
+
103
+ Returns a callable suitable for use as a CrewAI step_callback.
104
+ The callback checks the kill switch and raises RuntimeError
105
+ if the switch is active.
106
+
107
+ Args:
108
+ agent_id: The agent ID.
109
+
110
+ Returns:
111
+ Callback function.
112
+ """
113
+
114
+ def callback(step_output: Any) -> Any:
115
+ if not self.before_action(agent_id=agent_id):
116
+ raise RuntimeError(f"Kill switch active for agent={agent_id}, crew={self._crew_id}")
117
+ return step_output
118
+
119
+ return callback
120
+
121
+ def wrap_crew(
122
+ self,
123
+ crew: Any,
124
+ agent_id_field: str = "role",
125
+ ) -> Any:
126
+ """Wrap a CrewAI crew with kill switch protection.
127
+
128
+ This method patches the crew to check the kill switch before
129
+ each agent action. Returns the modified crew.
130
+
131
+ Args:
132
+ crew: The CrewAI Crew instance.
133
+ agent_id_field: Agent attribute to use as agent_id.
134
+
135
+ Returns:
136
+ The crew with kill switch protection applied.
137
+ """
138
+ if hasattr(crew, "step_callback"):
139
+ original = crew.step_callback
140
+
141
+ def wrapped_callback(step_output: Any) -> Any:
142
+ # Check kill switch
143
+ if not self.before_action(agent_id=self._crew_id):
144
+ raise RuntimeError(f"Kill switch active for crew={self._crew_id}")
145
+ if original is not None:
146
+ return original(step_output)
147
+ return step_output
148
+
149
+ crew.step_callback = wrapped_callback
150
+
151
+ return crew
@@ -0,0 +1,135 @@
1
+ """LangChain integration for agent-killswitch.
2
+
3
+ Provides a callback handler that checks the kill switch before and
4
+ after each LLM call, tool invocation, and chain execution.
5
+
6
+ Requires: pip install agent-killswitch[langchain]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any
13
+ from uuid import UUID
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ try:
18
+ from langchain_core.callbacks import BaseCallbackHandler
19
+ except ImportError:
20
+ # Create a stub so the class can be defined without the dependency
21
+ class BaseCallbackHandler: # type: ignore[no-redef]
22
+ """Stub for when langchain-core is not installed."""
23
+
24
+ pass
25
+
26
+
27
+ class KillSwitchCallbackHandler(BaseCallbackHandler):
28
+ """LangChain callback handler that enforces kill switch checks.
29
+
30
+ Checks the kill switch before each LLM call and tool invocation.
31
+ If the kill switch is active, raises an exception to halt the chain.
32
+
33
+ Example::
34
+
35
+ from langchain_openai import ChatOpenAI
36
+ from agent_killswitch import KillSwitch
37
+ from agent_killswitch.integrations.langchain import KillSwitchCallbackHandler
38
+
39
+ ks = KillSwitch()
40
+ handler = KillSwitchCallbackHandler(killswitch=ks, agent_id="my-agent")
41
+
42
+ llm = ChatOpenAI(callbacks=[handler])
43
+ llm.invoke("Hello") # Will check kill switch before calling
44
+
45
+ Args:
46
+ killswitch: The KillSwitch instance to check.
47
+ agent_id: The agent ID for scope-specific checks.
48
+ crew_id: Optional crew ID for crew scope checks.
49
+ raise_on_kill: If True, raise RuntimeError on kill. Default True.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ killswitch: Any,
55
+ agent_id: str | None = None,
56
+ crew_id: str | None = None,
57
+ raise_on_kill: bool = True,
58
+ ) -> None:
59
+ self._ks = killswitch
60
+ self._agent_id = agent_id
61
+ self._crew_id = crew_id
62
+ self._raise_on_kill = raise_on_kill
63
+
64
+ def _check(self, action: str) -> None:
65
+ """Check the kill switch and raise if active.
66
+
67
+ Args:
68
+ action: Description of the action being checked.
69
+ """
70
+ status = self._ks.check(
71
+ scope_target=self._agent_id,
72
+ crew_id=self._crew_id,
73
+ )
74
+ if status.is_killed:
75
+ msg = (
76
+ f"Kill switch active during {action}: level={status.level}, reason={status.reason}"
77
+ )
78
+ logger.warning("langchain_kill_switch_triggered", extra={"action": action})
79
+ if self._raise_on_kill:
80
+ raise RuntimeError(msg)
81
+
82
+ def on_llm_start(
83
+ self,
84
+ serialized: dict[str, Any],
85
+ prompts: list[str],
86
+ *,
87
+ run_id: UUID,
88
+ parent_run_id: UUID | None = None,
89
+ tags: list[str] | None = None,
90
+ metadata: dict[str, Any] | None = None,
91
+ **kwargs: Any,
92
+ ) -> None:
93
+ """Check kill switch before LLM call."""
94
+ self._check("llm_start")
95
+
96
+ def on_llm_end(
97
+ self,
98
+ response: Any,
99
+ *,
100
+ run_id: UUID,
101
+ parent_run_id: UUID | None = None,
102
+ tags: list[str] | None = None,
103
+ **kwargs: Any,
104
+ ) -> None:
105
+ """Check kill switch after LLM call."""
106
+ self._check("llm_end")
107
+
108
+ def on_tool_start(
109
+ self,
110
+ serialized: dict[str, Any],
111
+ input_str: str,
112
+ *,
113
+ run_id: UUID,
114
+ parent_run_id: UUID | None = None,
115
+ tags: list[str] | None = None,
116
+ metadata: dict[str, Any] | None = None,
117
+ inputs: dict[str, Any] | None = None,
118
+ **kwargs: Any,
119
+ ) -> None:
120
+ """Check kill switch before tool invocation."""
121
+ self._check("tool_start")
122
+
123
+ def on_chain_start(
124
+ self,
125
+ serialized: dict[str, Any],
126
+ inputs: dict[str, Any],
127
+ *,
128
+ run_id: UUID,
129
+ parent_run_id: UUID | None = None,
130
+ tags: list[str] | None = None,
131
+ metadata: dict[str, Any] | None = None,
132
+ **kwargs: Any,
133
+ ) -> None:
134
+ """Check kill switch before chain execution."""
135
+ self._check("chain_start")
@@ -0,0 +1,143 @@
1
+ """LangGraph integration for agent-killswitch.
2
+
3
+ Provides a killswitch check node and conditional edge for use
4
+ in LangGraph state graphs.
5
+
6
+ Requires: pip install agent-killswitch[langgraph]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class KillSwitchNode:
18
+ """A LangGraph node that checks the kill switch.
19
+
20
+ Use this as a node in a LangGraph StateGraph. If the kill switch
21
+ is active, it sets a flag in the state that can be used with
22
+ conditional edges to route to a termination node.
23
+
24
+ Example::
25
+
26
+ from langgraph.graph import StateGraph
27
+ from agent_killswitch import KillSwitch
28
+ from agent_killswitch.integrations.langgraph import KillSwitchNode
29
+
30
+ ks = KillSwitch()
31
+ ks_node = KillSwitchNode(killswitch=ks, agent_id="graph-agent")
32
+
33
+ graph = StateGraph(dict)
34
+ graph.add_node("check_kill", ks_node)
35
+ graph.add_node("agent_work", agent_node)
36
+ graph.add_node("terminated", termination_node)
37
+ graph.add_conditional_edges(
38
+ "check_kill",
39
+ ks_node.should_continue,
40
+ {"continue": "agent_work", "killed": "terminated"},
41
+ )
42
+
43
+ Args:
44
+ killswitch: The KillSwitch instance.
45
+ agent_id: Agent ID for scope checks.
46
+ crew_id: Optional crew ID for crew scope checks.
47
+ kill_state_key: Key in state dict to set when killed.
48
+ Default "kill_switch_active".
49
+ kill_level_key: Key in state dict to set with kill level.
50
+ Default "kill_level".
51
+ kill_reason_key: Key in state dict to set with kill reason.
52
+ Default "kill_reason".
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ killswitch: Any,
58
+ agent_id: str | None = None,
59
+ crew_id: str | None = None,
60
+ kill_state_key: str = "kill_switch_active",
61
+ kill_level_key: str = "kill_level",
62
+ kill_reason_key: str = "kill_reason",
63
+ ) -> None:
64
+ self._ks = killswitch
65
+ self._agent_id = agent_id
66
+ self._crew_id = crew_id
67
+ self._kill_state_key = kill_state_key
68
+ self._kill_level_key = kill_level_key
69
+ self._kill_reason_key = kill_reason_key
70
+
71
+ def __call__(self, state: dict[str, Any]) -> dict[str, Any]:
72
+ """Check the kill switch and update state.
73
+
74
+ Args:
75
+ state: The current LangGraph state.
76
+
77
+ Returns:
78
+ Updated state with kill switch status.
79
+ """
80
+ status = self._ks.check(
81
+ scope_target=self._agent_id,
82
+ crew_id=self._crew_id,
83
+ )
84
+
85
+ result = dict(state)
86
+ result[self._kill_state_key] = status.is_killed
87
+ if status.is_killed:
88
+ result[self._kill_level_key] = status.level.value if status.level else None
89
+ result[self._kill_reason_key] = status.reason or ""
90
+ logger.warning(
91
+ "langgraph_kill_switch_active",
92
+ extra={
93
+ "agent_id": self._agent_id,
94
+ "level": result[self._kill_level_key],
95
+ },
96
+ )
97
+ else:
98
+ result[self._kill_level_key] = None
99
+ result[self._kill_reason_key] = ""
100
+
101
+ return result
102
+
103
+ def should_continue(self, state: dict[str, Any]) -> str:
104
+ """Conditional edge function: 'continue' or 'killed'.
105
+
106
+ Use with graph.add_conditional_edges() to route based on
107
+ kill switch status.
108
+
109
+ Args:
110
+ state: The current LangGraph state.
111
+
112
+ Returns:
113
+ 'killed' if kill switch is active, 'continue' otherwise.
114
+ """
115
+ if state.get(self._kill_state_key, False):
116
+ return "killed"
117
+ return "continue"
118
+
119
+
120
+ def create_killswitch_node(
121
+ killswitch: Any,
122
+ agent_id: str | None = None,
123
+ crew_id: str | None = None,
124
+ ) -> tuple[Any, Any]:
125
+ """Convenience function to create a kill switch node and condition.
126
+
127
+ Returns a tuple of (node_function, condition_function) ready to
128
+ add to a LangGraph StateGraph.
129
+
130
+ Args:
131
+ killswitch: The KillSwitch instance.
132
+ agent_id: Agent ID for scope checks.
133
+ crew_id: Optional crew ID.
134
+
135
+ Returns:
136
+ Tuple of (node_callable, condition_callable).
137
+ """
138
+ node = KillSwitchNode(
139
+ killswitch=killswitch,
140
+ agent_id=agent_id,
141
+ crew_id=crew_id,
142
+ )
143
+ return node, node.should_continue
@@ -0,0 +1,151 @@
1
+ """OpenAI Agents SDK integration for agent-killswitch.
2
+
3
+ Provides a guardrail function and input guard compatible with the
4
+ OpenAI Agents SDK.
5
+
6
+ Requires: pip install agent-killswitch[openai]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class KillSwitchGuardrail:
18
+ """OpenAI Agents SDK guardrail that enforces kill switch checks.
19
+
20
+ Can be used as an input guardrail or output guardrail in the
21
+ OpenAI Agents SDK.
22
+
23
+ Example::
24
+
25
+ from agent_killswitch import KillSwitch
26
+ from agent_killswitch.integrations.openai_agents import KillSwitchGuardrail
27
+
28
+ ks = KillSwitch()
29
+ guardrail = KillSwitchGuardrail(killswitch=ks, agent_id="openai-agent")
30
+
31
+ # Use as input guardrail
32
+ result = guardrail.check_input("user message")
33
+ if not result["allowed"]:
34
+ print(f"Blocked: {result['reason']}")
35
+
36
+ Args:
37
+ killswitch: The KillSwitch instance.
38
+ agent_id: Agent ID for scope checks.
39
+ crew_id: Optional crew ID for crew scope checks.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ killswitch: Any,
45
+ agent_id: str | None = None,
46
+ crew_id: str | None = None,
47
+ ) -> None:
48
+ self._ks = killswitch
49
+ self._agent_id = agent_id
50
+ self._crew_id = crew_id
51
+
52
+ def check_input(self, user_input: Any) -> dict[str, Any]:
53
+ """Check kill switch before processing input.
54
+
55
+ Args:
56
+ user_input: The user input being processed.
57
+
58
+ Returns:
59
+ Dictionary with keys:
60
+ - 'allowed': bool
61
+ - 'reason': str (if blocked)
62
+ - 'level': str (if blocked)
63
+ """
64
+ status = self._ks.check(
65
+ scope_target=self._agent_id,
66
+ crew_id=self._crew_id,
67
+ )
68
+
69
+ if status.is_killed:
70
+ level_str = status.level.value if status.level else "unknown"
71
+ return {
72
+ "allowed": False,
73
+ "reason": status.reason or "Kill switch is active",
74
+ "level": level_str,
75
+ }
76
+
77
+ return {"allowed": True, "reason": "", "level": ""}
78
+
79
+ def check_output(self, output: Any) -> dict[str, Any]:
80
+ """Check kill switch before returning output.
81
+
82
+ Args:
83
+ output: The agent output being validated.
84
+
85
+ Returns:
86
+ Dictionary with keys:
87
+ - 'allowed': bool
88
+ - 'reason': str (if blocked)
89
+ - 'level': str (if blocked)
90
+ """
91
+ return self.check_input(output)
92
+
93
+ def as_guardrail_function(self) -> Any:
94
+ """Return a guardrail function compatible with the OpenAI SDK.
95
+
96
+ Returns:
97
+ An async function suitable for use as a guardrail.
98
+ """
99
+
100
+ async def guardrail_fn(
101
+ context: Any,
102
+ agent: Any,
103
+ input_data: Any,
104
+ ) -> Any | None:
105
+ """Guardrail function that checks the kill switch.
106
+
107
+ Args:
108
+ context: The run context.
109
+ agent: The agent being guarded.
110
+ input_data: The input being processed.
111
+
112
+ Returns:
113
+ None if allowed, or a GuardrailFunctionOutput if blocked.
114
+ """
115
+ status = self._ks.check(
116
+ scope_target=self._agent_id,
117
+ crew_id=self._crew_id,
118
+ )
119
+
120
+ if status.is_killed:
121
+ level_str = status.level.value if status.level else "unknown"
122
+ reason = status.reason or "Kill switch is active"
123
+ logger.warning(
124
+ "openai_agents_guardrail_blocked",
125
+ extra={
126
+ "agent_id": self._agent_id,
127
+ "level": level_str,
128
+ "reason": reason,
129
+ },
130
+ )
131
+
132
+ # Try to use the SDK's GuardrailFunctionOutput if available
133
+ try:
134
+ from openai.agents import GuardrailFunctionOutput, GuardrailTripwireTriggered
135
+
136
+ output = GuardrailFunctionOutput(
137
+ output_info={"blocked": True, "reason": reason, "level": level_str},
138
+ tripwire_triggered=True,
139
+ )
140
+ raise GuardrailTripwireTriggered(output)
141
+ except ImportError:
142
+ # The openai.agents extra is not installed. That is an expected,
143
+ # supported configuration - chaining the ImportError here would
144
+ # imply something went wrong, so suppress the context.
145
+ raise RuntimeError(
146
+ f"Kill switch active: level={level_str}, reason={reason}"
147
+ ) from None
148
+
149
+ return None
150
+
151
+ return guardrail_fn
File without changes