From 66be1630faa2335803969abea89b6c1ffc74fa40 Mon Sep 17 00:00:00 2001 From: Sameer6305 Date: Thu, 30 Jul 2026 20:43:26 +0530 Subject: [PATCH 1/2] fix(agno): fail fast on toolkit registration failures - #780 --- integrations/agno/decision_kit.py | 8 +++--- integrations/agno/kg_toolkit.py | 8 +++--- tests/integrations/agno/test_decision_kit.py | 28 ++++++++++++++++++-- tests/integrations/agno/test_kg_toolkit.py | 26 +++++++++++++++++- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/integrations/agno/decision_kit.py b/integrations/agno/decision_kit.py index bcb4e66e..cc64e9c6 100644 --- a/integrations/agno/decision_kit.py +++ b/integrations/agno/decision_kit.py @@ -123,12 +123,10 @@ class AgnoDecisionKit(_ToolkitBase): # type: ignore[misc] tools_to_register.append(self.check_policy) for fn in tools_to_register: - self._tools.append(fn) if AGNO_AVAILABLE: - try: - self.register(fn) - except Exception: - pass + self.register(fn) + if fn not in self._tools: + self._tools.append(fn) logger.info("AgnoDecisionKit initialised") diff --git a/integrations/agno/kg_toolkit.py b/integrations/agno/kg_toolkit.py index 75ee26ed..c36ea63f 100644 --- a/integrations/agno/kg_toolkit.py +++ b/integrations/agno/kg_toolkit.py @@ -122,12 +122,10 @@ class AgnoKGToolkit(_ToolkitBase): # type: ignore[misc] self.export_subgraph, ] for fn in tools_to_register: - self._tools.append(fn) if AGNO_AVAILABLE: - try: - self.register(fn) - except Exception: - pass + self.register(fn) + if fn not in self._tools: + self._tools.append(fn) logger.info("AgnoKGToolkit initialised (backend=%s)", graph_store_backend) diff --git a/tests/integrations/agno/test_decision_kit.py b/tests/integrations/agno/test_decision_kit.py index 8efd4830..3f13b6c0 100644 --- a/tests/integrations/agno/test_decision_kit.py +++ b/tests/integrations/agno/test_decision_kit.py @@ -8,7 +8,7 @@ import json import sys import types import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch # --------------------------------------------------------------------------- @@ -75,7 +75,31 @@ class TestAgnoDecisionKitInit(unittest.TestCase): def test_tools_registered(self): kit = AgnoDecisionKit(context=_make_context()) # Tools should be registered (Toolkit.register was called) - self.assertTrue(len(kit._tools) >= 5) + self.assertEqual(len(kit._tools), 6) + self.assertEqual(len(kit._tools), len(set(kit._tools))) + + def test_registration_invoked(self): + with patch.object(AgnoDecisionKit, "register") as mock_register: + AgnoDecisionKit(context=_make_context()) + self.assertEqual(mock_register.call_count, 6) + + def test_registration_failure_propagates(self): + with patch.object(AgnoDecisionKit, "register", side_effect=RuntimeError("Registration failed")): + with self.assertRaises(RuntimeError): + AgnoDecisionKit(context=_make_context()) + + def test_graceful_degradation_when_agno_unavailable(self): + with patch("integrations.agno.decision_kit.AGNO_AVAILABLE", False): + with patch.object(AgnoDecisionKit, "register") as mock_register: + kit = AgnoDecisionKit(context=_make_context()) + mock_register.assert_not_called() + self.assertEqual(len(kit._tools), 6) + self.assertEqual(len(kit._tools), len(set(kit._tools))) + + def test_no_duplicate_tools(self): + kit = AgnoDecisionKit(context=_make_context()) + self.assertEqual(len(kit._tools), len(set(kit._tools))) + self.assertEqual(len(kit._tools), 6) def test_policy_tool_can_be_disabled(self): kit = AgnoDecisionKit(context=_make_context(), enable_policy_check=False) diff --git a/tests/integrations/agno/test_kg_toolkit.py b/tests/integrations/agno/test_kg_toolkit.py index 8ddd25a9..d9dcdbfc 100644 --- a/tests/integrations/agno/test_kg_toolkit.py +++ b/tests/integrations/agno/test_kg_toolkit.py @@ -129,7 +129,31 @@ class TestAgnoKGToolkitInit(unittest.TestCase): def test_tools_registered(self): kit = AgnoKGToolkit() - self.assertTrue(len(kit._tools) >= 7) + self.assertEqual(len(kit._tools), 7) + self.assertEqual(len(kit._tools), len(set(kit._tools))) + + def test_registration_invoked(self): + with patch.object(AgnoKGToolkit, "register") as mock_register: + AgnoKGToolkit() + self.assertEqual(mock_register.call_count, 7) + + def test_registration_failure_propagates(self): + with patch.object(AgnoKGToolkit, "register", side_effect=RuntimeError("Registration failed")): + with self.assertRaises(RuntimeError): + AgnoKGToolkit() + + def test_graceful_degradation_when_agno_unavailable(self): + with patch("integrations.agno.kg_toolkit.AGNO_AVAILABLE", False): + with patch.object(AgnoKGToolkit, "register") as mock_register: + kit = AgnoKGToolkit() + mock_register.assert_not_called() + self.assertEqual(len(kit._tools), 7) + self.assertEqual(len(kit._tools), len(set(kit._tools))) + + def test_no_duplicate_tools(self): + kit = AgnoKGToolkit() + self.assertEqual(len(kit._tools), len(set(kit._tools))) + self.assertEqual(len(kit._tools), 7) def test_context_graph_attached(self): ctx = MagicMock() From 67aed4399732ffff29db9c39eb4a3c906040f386 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Fri, 31 Jul 2026 17:44:14 +0530 Subject: [PATCH 2/2] docs: add changelog entry for Agno toolkit fail-fast fix (#780, #818) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c57a8f62..bf2b43b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`AgnoDecisionKit`/`AgnoKGToolkit` silently swallowed Agno tool registration failures** (#780, #818) by @Sameer6305 and @KaifAhmad1 + - Removed the `try/except: pass` wrapped around `self.register(fn)` in both toolkits' `__init__`; when Agno is installed, a registration failure now propagates immediately instead of leaving the toolkit half-registered with no signal to the caller + - Graceful degradation when Agno isn't installed (`AGNO_AVAILABLE=False`) is unchanged — `_tools` is still populated so callers can introspect available tools without the package + - Fixed a related duplicate-entry bug: `self._tools` was appended to unconditionally *before* `register()` ran, which could double-count a tool when Agno's own `Toolkit.register()` also tracks it in `self._tools` + - This is a behavior change for callers that construct these toolkits expecting instantiation to always succeed — audited: no in-repo call site relies on the old silent-failure behavior + - Expanded `tests/integrations/agno/test_decision_kit.py` and `test_kg_toolkit.py` with coverage for registration invocation counts, failure propagation, graceful degradation, and no-duplicate-`_tools` assertions + - **`ProvenanceManager` duplicated the same checksum/persist/exception-swallow block across 4 tracking methods** (#784, #815) by @Sameer6305 and @KaifAhmad1 - Consolidated the repeated `entry.checksum = compute_checksum(entry)` / `try: self.storage.store(entry) except Exception: pass` block used by `track_entity`, `track_relationship`, `track_chunk`, and `track_property_source` into a single `ProvenanceManager._save_entry()` helper, preserving the existing graceful-failure behavior and the batch `_conn`/re-raise semantics from #807 - Added 4 regression tests (`tests/provenance/test_manager.py`) covering storage-failure swallowing for each of the four tracking methods, none of which had coverage for this path before