feat(api): implement websocket gateway and event streaming for phase 1

This commit is contained in:
2026-03-11 23:01:09 -04:00
parent 00a390ffab
commit 02e0fce548
8 changed files with 158 additions and 26 deletions

View File

@@ -9,7 +9,7 @@ between the GUI main thread and background workers:
- Thread-safe: Callbacks execute on emitter's thread
- Example: ai_client.py emits 'request_start' and 'response_received' events
2. SyncEventQueue: Producer-consumer pattern via queue.Queue
2. AsyncEventQueue: Producer-consumer pattern via queue.Queue
- Used for: Decoupled task submission where consumer polls at its own pace
- Thread-safe: Built on Python's thread-safe queue.Queue
- Example: Background workers submit tasks, main thread drains queue
@@ -21,16 +21,16 @@ between the GUI main thread and background workers:
Integration Points:
- ai_client.py: EventEmitter for API lifecycle events
- gui_2.py: Consumes events via _process_event_queue()
- multi_agent_conductor.py: Uses SyncEventQueue for state updates
- multi_agent_conductor.py: Uses AsyncEventQueue for state updates
- api_hooks.py: Pushes events to _api_event_queue for external visibility
Thread Safety:
- EventEmitter: NOT thread-safe for concurrent on/emit (use from single thread)
- SyncEventQueue: FULLY thread-safe (built on queue.Queue)
- AsyncEventQueue: FULLY thread-safe (built on queue.Queue)
- UserRequestEvent: Immutable, safe for concurrent access
"""
import queue
from typing import Callable, Any, Dict, List, Tuple
from typing import Callable, Any, Dict, List, Tuple, Optional
class EventEmitter:
"""
@@ -70,14 +70,16 @@ class EventEmitter:
"""Clears all registered listeners."""
self._listeners.clear()
class SyncEventQueue:
class AsyncEventQueue:
"""
Synchronous event queue for decoupled communication using queue.Queue.
(Named AsyncEventQueue for architectural consistency, but is synchronous)
"""
def __init__(self) -> None:
"""Initializes the SyncEventQueue with an internal queue.Queue."""
"""Initializes the AsyncEventQueue with an internal queue.Queue."""
self._queue: queue.Queue[Tuple[str, Any]] = queue.Queue()
self.websocket_server: Optional[Any] = None
def put(self, event_name: str, payload: Any = None) -> None:
"""
@@ -88,6 +90,8 @@ class SyncEventQueue:
payload: Optional data associated with the event.
"""
self._queue.put((event_name, payload))
if self.websocket_server:
self.websocket_server.broadcast("events", {"event": event_name, "payload": payload})
def get(self) -> Tuple[str, Any]:
"""
@@ -106,6 +110,9 @@ class SyncEventQueue:
"""Blocks until all items in the queue have been gotten and processed."""
self._queue.join()
# Alias for backward compatibility
SyncEventQueue = AsyncEventQueue
class UserRequestEvent:
"""
Payload for a user request event.
@@ -126,4 +133,3 @@ class UserRequestEvent:
"disc_text": self.disc_text,
"base_dir": self.base_dir
}