""" Session loop — the core driver for each active session. One SessionLoop instance runs per session as an asyncio Task. Never dispatches two LLM calls simultaneously. Starts immediately for autonomous mode; waits for first talker message otherwise. """ import asyncio import logging from typing import TYPE_CHECKING if TYPE_CHECKING: from fellowship.core.session import Session logger = logging.getLogger(__name__) class SessionLoop: def __init__(self, session: "Session") -> None: self.session = session self._task: asyncio.Task | None = None self._pause_event = asyncio.Event() self._pause_event.set() # Not paused by default def start(self) -> None: """Start the loop as a background asyncio Task.""" self._task = asyncio.create_task(self._run(), name=f"loop-{self.session.token[:8]}") def stop(self) -> None: """Cancel the loop task.""" if self._task: self._task.cancel() def pause(self) -> None: """Pause the loop. Clears the internal event so the loop blocks.""" self._pause_event.clear() def resume(self) -> None: """Resume a paused loop.""" self._pause_event.set() async def _run(self) -> None: """ Main loop body. Runs until the session ends or the task is cancelled. Each iteration: 1. Wait if paused. 2. Check for pending talker messages (all modes). 3. Determine next bot speaker (round_robin or orchestrator). 4. Execute bot turn. 5. Check end conditions. """ raise NotImplementedError async def _handle_talker_message(self) -> bool: """ Drain one message from the talker queue into history. Returns True if a message was processed. """ raise NotImplementedError async def _check_end_conditions(self) -> bool: """ Check all configured end conditions (max_turns, max_time, max_context_tokens). Returns True if the session should end. """ raise NotImplementedError