"""Blocking bridge for interactive tool-ui components: AskUI parks here until the user answers in the transcript (or the wait times out). Keyed by (session_id, component props.id), so the frontend can respond without ever learning a server-side request id.""" import asyncio from typing import Any, Dict, Optional, Tuple from pydantic import BaseModel, ConfigDict, InstanceOf from typeguard import typechecked MAX_PENDING = 50 MAX_WAIT_SECONDS = 600.0 class PendingUiRequest(BaseModel): model_config = ConfigDict(validate_assignment=True) event: InstanceOf[asyncio.Event] response: Optional[Dict[str, Any]] = None p_pending: Dict[Tuple[str, str], PendingUiRequest] = {} @typechecked async def wait_for_ui_response(session_id: str, component_id: str, timeout_s: float) -> Optional[Dict[str, Any]]: """Registers the request and blocks until respond_to_ui_request fires it; None on timeout.""" if len(p_pending) >= MAX_PENDING: raise ValueError("too many pending UI requests") key = (session_id, component_id) # A retried tool call for the same component replaces the stale wait; the old waiter times out. pending = PendingUiRequest(event=asyncio.Event()) p_pending[key] = pending try: await asyncio.wait_for(pending.event.wait(), timeout=min(timeout_s, MAX_WAIT_SECONDS)) return pending.response except asyncio.TimeoutError: return None finally: if p_pending.get(key) is pending: p_pending.pop(key, None) @typechecked def respond_to_ui_request(session_id: str, component_id: str, response: Dict[str, Any]) -> bool: """Delivers the user's answer to the parked wait; False when nothing is waiting.""" pending = p_pending.get((session_id, component_id)) if pending is None: return False pending.response = response pending.event.set() return True