Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions src/smallestai/atoms/crew/nodes/output_crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,31 @@ 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):
self.context.add_message({"role": event.role, "content": event.content})
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):
Expand Down
63 changes: 63 additions & 0 deletions tests/custom/test_output_node_on_event_hook.py
Original file line number Diff line number Diff line change
@@ -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()
Loading