logic-engine / ace /integrations /mcp /registry.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
3.58 kB
import logging
import time
import asyncio
from dataclasses import dataclass, field
from typing import Dict, Any
from ace.runners import ACELiteLLM
from ace.integrations.mcp.config import MCPServerConfig
from ace.integrations.mcp.errors import SessionNotFoundError
logger = logging.getLogger(__name__)
@dataclass
class Session:
session_id: str
runner: ACELiteLLM
created_at: float = field(default_factory=time.time)
last_accessed: float = field(default_factory=time.time)
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
class SessionRegistry:
def __init__(self, config: MCPServerConfig):
self.config = config
self._sessions: Dict[str, Session] = {}
self._registry_lock = asyncio.Lock()
async def get_or_create(
self, session_id: str, model: str | None = None, **runner_kwargs: Any
) -> Session:
"""Get an existing session or create a new one, sweeping expired sessions first."""
async with self._registry_lock:
expired = self._collect_expired()
if session_id in self._sessions:
session = self._sessions[session_id]
session.last_accessed = time.time()
result = session
else:
# Create new runner
target_model = model or self.config.default_model
runner = ACELiteLLM.from_model(target_model, **runner_kwargs)
session = Session(session_id=session_id, runner=runner)
self._sessions[session_id] = session
result = session
# Drain outside the lock so other callers aren't blocked
self._drain_sessions(expired)
return result
async def get(self, session_id: str) -> Session:
"""Get an existing session. Raises SessionNotFoundError if not found."""
async with self._registry_lock:
expired = self._collect_expired()
if session_id not in self._sessions:
# Drain before raising so we don't leak
self._drain_sessions(expired)
raise SessionNotFoundError(session_id)
session = self._sessions[session_id]
session.last_accessed = time.time()
self._drain_sessions(expired)
return session
async def delete(self, session_id: str) -> None:
"""Delete a session if it exists."""
async with self._registry_lock:
session = self._sessions.pop(session_id, None)
if session is not None:
self._drain_sessions([session])
def _collect_expired(self) -> list[Session]:
"""Remove and return sessions that have exceeded the TTL.
Must be called while holding ``_registry_lock``.
"""
now = time.time()
ttl = self.config.session_ttl_seconds
expired_ids = [
sid
for sid, session in self._sessions.items()
if now - session.last_accessed > ttl
]
return [self._sessions.pop(sid) for sid in expired_ids]
@staticmethod
def _drain_sessions(sessions: list[Session]) -> None:
"""Best-effort wait for any in-progress background learning."""
for session in sessions:
try:
session.runner.wait_for_background(timeout=2.0)
except Exception:
logger.debug(
"Failed to drain session %s", session.session_id, exc_info=True
)