refactor: decouple webui runtime state via events

This commit is contained in:
chengyongru
2026-06-01 23:00:53 +08:00
committed by Xubin Ren
parent 0c6ce80aeb
commit 628b250e9a
11 changed files with 559 additions and 153 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Progress callback helpers that publish through the message bus."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Any
from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.bus.queue import MessageBus
def build_bus_progress_callback(
bus: MessageBus,
msg: InboundMessage,
) -> Callable[..., Awaitable[None]]:
"""Return the bus progress callback for agent runtime events."""
async def _publish_progress(
content: str,
*,
tool_hint: bool = False,
tool_events: list[dict[str, Any]] | None = None,
file_edit_events: list[dict[str, Any]] | None = None,
reasoning: bool = False,
reasoning_end: bool = False,
) -> None:
meta = dict(msg.metadata or {})
meta["_progress"] = True
meta["_tool_hint"] = tool_hint
if reasoning:
meta["_reasoning_delta"] = True
if reasoning_end:
meta["_reasoning_end"] = True
if tool_events:
meta["_tool_events"] = tool_events
if file_edit_events:
meta["_file_edit_events"] = file_edit_events
await bus.publish_outbound(
OutboundMessage(
channel=msg.channel,
chat_id=msg.chat_id,
content=content,
metadata=meta,
)
)
if msg.channel == "websocket":
async def _websocket_progress(
content: str,
*,
tool_hint: bool = False,
tool_events: list[dict[str, Any]] | None = None,
file_edit_events: list[dict[str, Any]] | None = None,
reasoning: bool = False,
reasoning_end: bool = False,
) -> None:
await _publish_progress(
content,
tool_hint=tool_hint,
tool_events=tool_events,
file_edit_events=file_edit_events,
reasoning=reasoning,
reasoning_end=reasoning_end,
)
return _websocket_progress
async def _bus_progress(
content: str,
*,
tool_hint: bool = False,
tool_events: list[dict[str, Any]] | None = None,
reasoning: bool = False,
reasoning_end: bool = False,
) -> None:
await _publish_progress(
content,
tool_hint=tool_hint,
tool_events=tool_events,
reasoning=reasoning,
reasoning_end=reasoning_end,
)
return _bus_progress
+116
View File
@@ -0,0 +1,116 @@
"""Runtime event bus for agent state notifications.
This bus is separate from :mod:`nanobot.bus.queue`: message bus events are
user/chat delivery, while runtime events are in-process state notifications
that optional subscribers such as WebUI adapters may render.
"""
from __future__ import annotations
import asyncio
import contextlib
import inspect
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any
from loguru import logger
@dataclass(frozen=True)
class RuntimeEventContext:
"""Routing context common to turn-scoped runtime events."""
channel: str
chat_id: str
session_key: str
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class SessionTurnStarted:
"""A user/system turn has loaded its session and is about to build context."""
context: RuntimeEventContext
@dataclass(frozen=True)
class TurnRunStatusChanged:
"""Visible run status changed for a turn."""
context: RuntimeEventContext
status: str
started_at: float | None = None
@dataclass(frozen=True)
class TurnCompleted:
"""A turn has delivered its final user-visible response."""
context: RuntimeEventContext
latency_ms: int | None = None
runtime: Any | None = None
@dataclass(frozen=True)
class GoalStateChanged:
"""A session's sustained-goal state changed."""
context: RuntimeEventContext
session_metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class RuntimeModelChanged:
"""The active runtime model/preset changed."""
model: str
model_preset: str | None
RuntimeEvent = (
SessionTurnStarted
| TurnRunStatusChanged
| TurnCompleted
| GoalStateChanged
| RuntimeModelChanged
)
RuntimeEventHandler = Callable[[RuntimeEvent], Awaitable[None] | None]
class RuntimeEventBus:
"""Small in-process pub/sub bus for runtime state.
Subscribers run in registration order. ``publish`` awaits async handlers so
callers can preserve ordering when a runtime event must follow a user
message. ``publish_nowait`` is available for synchronous call sites.
"""
def __init__(self) -> None:
self._handlers: list[RuntimeEventHandler] = []
def subscribe(self, handler: RuntimeEventHandler) -> Callable[[], None]:
self._handlers.append(handler)
def _unsubscribe() -> None:
with contextlib.suppress(ValueError):
self._handlers.remove(handler)
return _unsubscribe
async def publish(self, event: RuntimeEvent) -> None:
for handler in list(self._handlers):
try:
result = handler(event)
if inspect.isawaitable(result):
await result
except Exception:
logger.exception("runtime event handler failed for {}", type(event).__name__)
def publish_nowait(self, event: RuntimeEvent) -> None:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
logger.debug("dropping runtime event without a running loop: {}", type(event).__name__)
return
loop.create_task(self.publish(event))