diff --git a/libs/agno/agno/agent/_run.py b/libs/agno/agno/agent/_run.py index f8419d316d..ed5a4c9c4a 100644 --- a/libs/agno/agno/agent/_run.py +++ b/libs/agno/agno/agent/_run.py @@ -1268,13 +1268,12 @@ def run_dispatch( # Validate input against input_schema if provided validated_input = validate_input(input, agent.input_schema) - # Normalise hook & guardails - if not agent._hooks_normalised: - if agent.pre_hooks: - agent.pre_hooks = normalize_pre_hooks(agent.pre_hooks) # type: ignore - if agent.post_hooks: - agent.post_hooks = normalize_post_hooks(agent.post_hooks) # type: ignore - agent._hooks_normalised = True + # Normalise hooks & guardrails on every run. Hooks may be reassigned between + # runs, so always normalise rather than caching behind a flag. + if agent.pre_hooks: + agent.pre_hooks = normalize_pre_hooks(agent.pre_hooks) # type: ignore + if agent.post_hooks: + agent.post_hooks = normalize_post_hooks(agent.post_hooks) # type: ignore # Initialize session session_id, user_id = initialize_session(agent, session_id=session_id, user_id=user_id) @@ -2641,13 +2640,12 @@ def arun_dispatch( # type: ignore # 2. Validate input against input_schema if provided validated_input = validate_input(input, agent.input_schema) - # Normalise hooks & guardails - if not agent._hooks_normalised: - if agent.pre_hooks: - agent.pre_hooks = normalize_pre_hooks(agent.pre_hooks, async_mode=True) # type: ignore - if agent.post_hooks: - agent.post_hooks = normalize_post_hooks(agent.post_hooks, async_mode=True) # type: ignore - agent._hooks_normalised = True + # Normalise hooks & guardrails on every run. Hooks may be reassigned between + # runs, so always normalise rather than caching behind a flag. + if agent.pre_hooks: + agent.pre_hooks = normalize_pre_hooks(agent.pre_hooks, async_mode=True) # type: ignore + if agent.post_hooks: + agent.post_hooks = normalize_post_hooks(agent.post_hooks, async_mode=True) # type: ignore # Initialize session session_id, user_id = initialize_session(agent, session_id=session_id, user_id=user_id) diff --git a/libs/agno/agno/agent/agent.py b/libs/agno/agno/agent/agent.py index 25d9b08821..aee8672bce 100644 --- a/libs/agno/agno/agent/agent.py +++ b/libs/agno/agno/agent/agent.py @@ -689,8 +689,6 @@ def __init__( self._formatter: Optional[SafeFormatter] = None - self._hooks_normalised = False - self._mcp_tools_initialized_on_run: List[Any] = [] self._connectable_tools_initialized_on_run: List[Any] = [] diff --git a/libs/agno/agno/team/_init.py b/libs/agno/agno/team/_init.py index 0b8e696f4f..454db5dda6 100644 --- a/libs/agno/agno/team/_init.py +++ b/libs/agno/agno/team/_init.py @@ -408,8 +408,6 @@ def __init__( team._formatter = None - team._hooks_normalised = False - # List of MCP tools that were initialized on the last run team._mcp_tools_initialized_on_run = [] # List of connectable tools that were initialized on the last run diff --git a/libs/agno/agno/team/_run.py b/libs/agno/agno/team/_run.py index 3da55da5ac..b7ca4c0961 100644 --- a/libs/agno/agno/team/_run.py +++ b/libs/agno/agno/team/_run.py @@ -1815,13 +1815,12 @@ def run_dispatch( # Register run for cancellation tracking (after validation succeeds) register_run(run_id) # type: ignore - # Normalise hook & guardails - if not team._hooks_normalised: - if team.pre_hooks: - team.pre_hooks = normalize_pre_hooks(team.pre_hooks) # type: ignore - if team.post_hooks: - team.post_hooks = normalize_post_hooks(team.post_hooks) # type: ignore - team._hooks_normalised = True + # Normalise hooks & guardrails on every run. Hooks may be reassigned between + # runs, so always normalise rather than caching behind a flag. + if team.pre_hooks: + team.pre_hooks = normalize_pre_hooks(team.pre_hooks) # type: ignore + if team.post_hooks: + team.post_hooks = normalize_post_hooks(team.post_hooks) # type: ignore session_id, user_id = _initialize_session(team, session_id=session_id, user_id=user_id) @@ -3948,13 +3947,12 @@ def arun_dispatch( # type: ignore # Validate input against input_schema if provided validated_input = validate_input(input, team.input_schema) - # Normalise hook & guardails - if not team._hooks_normalised: - if team.pre_hooks: - team.pre_hooks = normalize_pre_hooks(team.pre_hooks, async_mode=True) # type: ignore - if team.post_hooks: - team.post_hooks = normalize_post_hooks(team.post_hooks, async_mode=True) # type: ignore - team._hooks_normalised = True + # Normalise hooks & guardrails on every run. Hooks may be reassigned between + # runs, so always normalise rather than caching behind a flag. + if team.pre_hooks: + team.pre_hooks = normalize_pre_hooks(team.pre_hooks, async_mode=True) # type: ignore + if team.post_hooks: + team.post_hooks = normalize_post_hooks(team.post_hooks, async_mode=True) # type: ignore session_id, user_id = _initialize_session(team, session_id=session_id, user_id=user_id) diff --git a/libs/agno/agno/team/team.py b/libs/agno/agno/team/team.py index 214b796234..49e46a49a7 100644 --- a/libs/agno/agno/team/team.py +++ b/libs/agno/agno/team/team.py @@ -409,8 +409,6 @@ class Team: _member_response_model: Optional[Union[Type[BaseModel], Dict[str, Any]]] = None # Safe formatter for template resolution _formatter: Optional[Any] = None - # Hooks normalised flag - _hooks_normalised: bool = False # MCP tools initialized on the last run _mcp_tools_initialized_on_run: Optional[List[Any]] = None # Connectable tools initialized on the last run diff --git a/libs/agno/tests/unit/agent/test_hooks_normalised.py b/libs/agno/tests/unit/agent/test_hooks_normalised.py new file mode 100644 index 0000000000..e2e2bd73dc --- /dev/null +++ b/libs/agno/tests/unit/agent/test_hooks_normalised.py @@ -0,0 +1,33 @@ +from agno.guardrails.base import BaseGuardrail +from agno.run.agent import RunInput +from agno.utils.hooks import normalize_pre_hooks + + +class _StubGuardrail(BaseGuardrail): + name: str = "stub" + + def check(self, run_input: RunInput) -> None: + pass + + async def async_check(self, run_input: RunInput) -> None: + pass + + +def test_pre_hooks_normalised_on_first_run(): + guardrail = _StubGuardrail() + hooks = normalize_pre_hooks([guardrail], async_mode=True) + assert hooks is not None + assert hooks[0] == guardrail.async_check + + +def test_pre_hooks_normalised_after_reassignment(): + # Re-normalising with a new guardrail instance must return its bound method + guardrail = _StubGuardrail() + hooks = normalize_pre_hooks([guardrail], async_mode=True) + assert hooks is not None + assert hooks[0] == guardrail.async_check + + guardrail2 = _StubGuardrail() + hooks2 = normalize_pre_hooks([guardrail2], async_mode=True) + assert hooks2 is not None + assert hooks2[0] == guardrail2.async_check diff --git a/libs/agno/tests/unit/team/test_hooks_normalised.py b/libs/agno/tests/unit/team/test_hooks_normalised.py new file mode 100644 index 0000000000..e2e2bd73dc --- /dev/null +++ b/libs/agno/tests/unit/team/test_hooks_normalised.py @@ -0,0 +1,33 @@ +from agno.guardrails.base import BaseGuardrail +from agno.run.agent import RunInput +from agno.utils.hooks import normalize_pre_hooks + + +class _StubGuardrail(BaseGuardrail): + name: str = "stub" + + def check(self, run_input: RunInput) -> None: + pass + + async def async_check(self, run_input: RunInput) -> None: + pass + + +def test_pre_hooks_normalised_on_first_run(): + guardrail = _StubGuardrail() + hooks = normalize_pre_hooks([guardrail], async_mode=True) + assert hooks is not None + assert hooks[0] == guardrail.async_check + + +def test_pre_hooks_normalised_after_reassignment(): + # Re-normalising with a new guardrail instance must return its bound method + guardrail = _StubGuardrail() + hooks = normalize_pre_hooks([guardrail], async_mode=True) + assert hooks is not None + assert hooks[0] == guardrail.async_check + + guardrail2 = _StubGuardrail() + hooks2 = normalize_pre_hooks([guardrail2], async_mode=True) + assert hooks2 is not None + assert hooks2[0] == guardrail2.async_check