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
9 changes: 8 additions & 1 deletion src/smallestai/atoms/crew/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,15 @@ async def start(self) -> None:

self._running = True

await self._start_nodes(self._init_event, self.task_manager)
# Send Ready BEFORE starting nodes. A node's start() may emit a speak
# event (e.g. a greeting); if that reaches the platform before Ready, the
# platform's connect handshake sees a non-Ready first frame and never
# spawns its receive loop, silencing the whole call. Ready-first keeps any
# early speak legal. Incoming events still buffer on the websocket until
# our own receive loop starts below (after nodes are up), so nothing is
# lost by ordering Ready first.
await self.send_to_websocket(SDKAgentReadyEvent())
await self._start_nodes(self._init_event, self.task_manager)

self._receive_loop_task = self.task_manager.create_task(
self._receive_loop(), name="receive_loop"
Expand Down
54 changes: 54 additions & 0 deletions tests/custom/test_crew_session_ready_ordering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Regression test: CrewSession.start() must send SDKAgentReadyEvent BEFORE
starting nodes, so a node's start() that emits a speak (e.g. a greeting) doesn't
reach the platform before Ready and poison the connect handshake."""
import unittest
from unittest import mock

from smallestai.atoms.crew.session import CrewSession
from smallestai.atoms.crew.nodes import OutputCrewNode


class _OrderNode(OutputCrewNode):
def __init__(self, order):
super().__init__(name="rec")
self._order = order

async def start(self, init_event, task_manager):
await super().start(init_event, task_manager)
self._order.append("node_started")

async def generate_response(self):
if False:
yield "" # never runs; satisfies the abstract async-generator


class ReadyBeforeNodesTest(unittest.IsolatedAsyncioTestCase):
async def test_ready_sent_before_nodes_start(self):
order = []
session = CrewSession(
websocket=mock.AsyncMock(), session_id="t", setup_handler=None
)
session._init_event = mock.MagicMock()
session.task_manager = mock.MagicMock()
session.task_manager.create_task = mock.MagicMock() # don't run receive loop
session._receive_loop = mock.MagicMock() # avoid unawaited-coroutine warning

async def _record_ready(event):
order.append("ready_sent")

session.send_to_websocket = _record_ready
session.add_node(_OrderNode(order))

await session.start()

self.assertIn("ready_sent", order)
self.assertIn("node_started", order)
self.assertLess(
order.index("ready_sent"),
order.index("node_started"),
f"Ready must be sent before nodes start; got order={order}",
)


if __name__ == "__main__":
unittest.main()
Loading