diff --git a/src/smallestai/atoms/crew/nodes/output_crew.py b/src/smallestai/atoms/crew/nodes/output_crew.py index 0f4b860d..bfb86dab 100644 --- a/src/smallestai/atoms/crew/nodes/output_crew.py +++ b/src/smallestai/atoms/crew/nodes/output_crew.py @@ -75,16 +75,10 @@ async def _update_settings(self, settings: Dict[str, Any]): logger.debug(f"[{self.name}] Updating setting {key} to {value}") setattr(self.settings, key, value) - async def process_event(self, event: SDKEvent): - """ - Route events to appropriate handlers. - - Handles: - - LLMRequestEvent -> Start generation - """ - - await super().process_event(event) - + async def _route_framework_event(self, event: SDKEvent): + """Framework-owned event routing. Always runs, independent of any user + `on_event` override, so a subclass can't accidentally silence the agent + (e.g. by dropping the LLM-request -> generate_response path).""" if isinstance(event, SDKSystemLLMRequestEvent): await self._handle_llm_request() elif isinstance(event, SDKAgentTranscriptUpdateEvent): @@ -92,6 +86,20 @@ async def process_event(self, event: SDKEvent): elif isinstance(event, SDKSystemUpdateOutputAgentSettingsEvent): await self._update_settings(event.settings) + async def on_event(self, event: SDKEvent): + """Override this to react to events. This is the safe extension point: + the framework routing (interrupts, LLM requests, transcript/context and + settings updates) already ran before this is called, so you do NOT need + to call super(). Prefer this over overriding `process_event`, which + carries that framework routing — dropping it silences the agent.""" + pass + + async def process_event(self, event: SDKEvent): + """Framework dispatch: interrupt handling, output-node routing, the user + `on_event` hook, then forward. Prefer overriding `on_event` over this.""" + await super().process_event(event) + await self._route_framework_event(event) + await self.on_event(event) await self.send_event(event) async def speak(self, text: str): diff --git a/tests/custom/test_output_node_on_event_hook.py b/tests/custom/test_output_node_on_event_hook.py new file mode 100644 index 00000000..fa077ce3 --- /dev/null +++ b/tests/custom/test_output_node_on_event_hook.py @@ -0,0 +1,63 @@ +"""OutputCrewNode: framework routing must run even when a subclass overrides the +user hook without super(). Overriding `on_event` (the documented extension point) +must NOT silence the LLM-request -> generate_response path, and existing +`process_event`-with-super() overrides must keep working (backward compat).""" +import unittest +from unittest import mock + +from smallestai.atoms.crew.nodes import OutputCrewNode +from smallestai.atoms.crew.events import SDKSystemLLMRequestEvent + + +class _OnEventAgent(OutputCrewNode): + """Recommended pattern: override on_event, no super().""" + def __init__(self, order): + super().__init__(name="a") + self._order = order + + async def on_event(self, event): + self._order.append("on_event") + + async def generate_response(self): + if False: + yield "" + + +class _LegacyProcessEventAgent(OutputCrewNode): + """Legacy pattern: override process_event and call super().""" + def __init__(self, order): + super().__init__(name="b") + self._order = order + + async def process_event(self, event): + await super().process_event(event) + self._order.append("legacy_process_event") + + async def generate_response(self): + if False: + yield "" + + +class OnEventHookTest(unittest.IsolatedAsyncioTestCase): + async def test_on_event_override_keeps_framework_routing(self): + order = [] + a = _OnEventAgent(order) + a._handle_llm_request = mock.AsyncMock(side_effect=lambda: order.append("llm")) + a.send_event = mock.AsyncMock() + await a.process_event(SDKSystemLLMRequestEvent()) + self.assertIn("llm", order, "framework LLM routing must run") + self.assertIn("on_event", order, "user hook must run") + self.assertLess(order.index("llm"), order.index("on_event"), "framework runs before user hook") + + async def test_legacy_process_event_with_super_still_routes(self): + order = [] + b = _LegacyProcessEventAgent(order) + b._handle_llm_request = mock.AsyncMock(side_effect=lambda: order.append("llm")) + b.send_event = mock.AsyncMock() + await b.process_event(SDKSystemLLMRequestEvent()) + self.assertIn("llm", order, "backward-compat: super() still routes") + self.assertIn("legacy_process_event", order) + + +if __name__ == "__main__": + unittest.main()