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
@@ -7,6 +7,7 @@ from nanobot.agent.loop import AgentLoop
from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.providers.base import GenerationSettings, LLMResponse
from nanobot.session.webui_turns import WebuiTurnCoordinator
def _make_loop(tmp_path):
@@ -25,6 +26,11 @@ def _make_loop(tmp_path):
workspace=tmp_path,
model="test-model",
)
WebuiTurnCoordinator(
bus=bus,
sessions=loop.sessions,
schedule_background=lambda coro: loop._schedule_background(coro),
).subscribe(loop.runtime_events)
loop.tools.get_definitions = MagicMock(return_value=[])
return loop
+15
View File
@@ -11,6 +11,7 @@ from nanobot.agent.loop import AgentLoop
from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.providers.base import LLMResponse, ToolCallRequest
from nanobot.session.webui_turns import WebuiTurnCoordinator
from nanobot.utils.progress_events import (
invoke_file_edit_progress,
on_progress_accepts_file_edit_events,
@@ -24,6 +25,15 @@ def _make_loop(tmp_path: Path) -> AgentLoop:
return AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
def _attach_webui_runtime_events(loop: AgentLoop, bus: MessageBus) -> None:
coordinator = WebuiTurnCoordinator(
bus=bus,
sessions=loop.sessions,
schedule_background=lambda coro: loop._schedule_background(coro),
)
coordinator.subscribe(loop.runtime_events)
class TestToolEventProgress:
"""_run_agent_loop emits structured tool_events via on_progress."""
@@ -456,6 +466,7 @@ class TestToolEventProgress:
provider.chat_stream_with_retry = chat_stream_with_retry
provider.chat_with_retry = AsyncMock()
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="openai-codex/gpt-5.5")
_attach_webui_runtime_events(loop, bus)
loop.tools.get_definitions = MagicMock(return_value=[])
loop.consolidator.maybe_consolidate_by_tokens = AsyncMock(return_value=False) # type: ignore[method-assign]
@@ -549,6 +560,7 @@ class TestToolEventProgress:
provider.get_default_model.return_value = "test-model"
provider.chat_with_retry = AsyncMock(return_value=LLMResponse(content="Done", tool_calls=[]))
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
_attach_webui_runtime_events(loop, bus)
loop.tools.get_definitions = MagicMock(return_value=[])
loop.consolidator.maybe_consolidate_by_tokens = AsyncMock(return_value=False) # type: ignore[method-assign]
@@ -593,6 +605,7 @@ class TestToolEventProgress:
provider.chat_with_retry = AsyncMock(side_effect=chat_with_retry)
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
_attach_webui_runtime_events(loop, bus)
loop.tools.get_definitions = MagicMock(return_value=[])
loop.consolidator.maybe_consolidate_by_tokens = AsyncMock(return_value=False) # type: ignore[method-assign]
@@ -641,6 +654,7 @@ class TestToolEventProgress:
provider.get_default_model.return_value = "test-model"
provider.chat_with_retry = AsyncMock(return_value=LLMResponse(content="Done", tool_calls=[]))
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
_attach_webui_runtime_events(loop, bus)
loop.tools.get_definitions = MagicMock(return_value=[])
loop.consolidator.maybe_consolidate_by_tokens = AsyncMock(return_value=False) # type: ignore[method-assign]
@@ -693,6 +707,7 @@ class TestToolEventProgress:
provider.get_default_model.return_value = "test-model"
provider.chat_with_retry = AsyncMock(return_value=LLMResponse(content="Done", tool_calls=[]))
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
_attach_webui_runtime_events(loop, bus)
async def fake_title_after_turn(**_kwargs: object) -> bool:
raise AssertionError("command-only turns should not generate titles")
+7 -1
View File
@@ -39,7 +39,13 @@ def _make_full_loop(tmp_path: Path) -> AgentLoop:
provider = MagicMock()
provider.get_default_model.return_value = "test-model"
provider.chat_with_retry = AsyncMock(return_value=LLMResponse(content="Test title"))
return AgentLoop(bus=MessageBus(), provider=provider, workspace=tmp_path, model="test-model")
loop = AgentLoop(bus=MessageBus(), provider=provider, workspace=tmp_path, model="test-model")
WebuiTurnCoordinator(
bus=loop.bus,
sessions=loop.sessions,
schedule_background=lambda coro: loop._schedule_background(coro),
).subscribe(loop.runtime_events)
return loop
def test_agent_loop_llm_runtime_reflects_current_provider_and_model(tmp_path: Path) -> None:
+17 -3
View File
@@ -14,8 +14,10 @@ from nanobot.agent.tools.long_task import (
LongTaskTool,
)
from nanobot.bus.queue import MessageBus
from nanobot.bus.runtime_events import RuntimeEventBus
from nanobot.session.goal_state import GOAL_STATE_KEY
from nanobot.session.manager import SessionManager
from nanobot.session.webui_turns import WebuiTurnCoordinator
def _tools(sm: SessionManager) -> tuple[LongTaskTool, CompleteGoalTool]:
@@ -120,8 +122,14 @@ async def test_goal_tools_context_isolated_across_tool_types(tmp_path):
async def test_long_task_publishes_goal_state_ws_after_save(tmp_path):
bus = MagicMock()
bus.publish_outbound = AsyncMock()
runtime_events = RuntimeEventBus()
sm = SessionManager(tmp_path)
lt = LongTaskTool(sessions=sm, bus=bus)
WebuiTurnCoordinator(
bus=bus,
sessions=sm,
schedule_background=lambda _coro: None,
).subscribe(runtime_events)
lt = LongTaskTool(sessions=sm, runtime_events=runtime_events)
rc = RequestContext(
channel="websocket",
chat_id="chat-99",
@@ -148,9 +156,15 @@ async def test_long_task_publishes_goal_state_ws_after_save(tmp_path):
async def test_complete_goal_publishes_inactive_goal_state_ws(tmp_path):
bus = MagicMock()
bus.publish_outbound = AsyncMock()
runtime_events = RuntimeEventBus()
sm = SessionManager(tmp_path)
lt = LongTaskTool(sessions=sm, bus=bus)
cg = CompleteGoalTool(sessions=sm, bus=bus)
WebuiTurnCoordinator(
bus=bus,
sessions=sm,
schedule_background=lambda _coro: None,
).subscribe(runtime_events)
lt = LongTaskTool(sessions=sm, runtime_events=runtime_events)
cg = CompleteGoalTool(sessions=sm, runtime_events=runtime_events)
rc = RequestContext(
channel="websocket",
chat_id="chat-z",