mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 06:14:02 +00:00
fix: close websocket turns after errors
This commit is contained in:
parent
ebc8c9faf9
commit
0042f68f94
@ -6,6 +6,8 @@ These rules govern architectural decisions. When adding a feature or fixing a bu
|
|||||||
|
|
||||||
New capabilities should be added via `channels/`, `tools/`, skills, or MCP servers. The files `agent/loop.py` and `agent/runner.py` form the critical core path; changes there should be minimal and justified. If a feature can live in a channel adapter, a tool, or an external MCP server, it should not be inlined into the agent loop.
|
New capabilities should be added via `channels/`, `tools/`, skills, or MCP servers. The files `agent/loop.py` and `agent/runner.py` form the critical core path; changes there should be minimal and justified. If a feature can live in a channel adapter, a tool, or an external MCP server, it should not be inlined into the agent loop.
|
||||||
|
|
||||||
|
Runtime state fan-out follows the same boundary. `AgentLoop` may publish generic runtime events from `nanobot.bus.runtime_events` for turn/run/model/goal state changes, but WebUI/WebSocket wire details such as `_turn_end`, `_goal_status`, title refreshes, and goal-state sync belong in `nanobot.session.webui_turns.WebuiTurnCoordinator` or the relevant channel adapter.
|
||||||
|
|
||||||
## Less structure, more intelligence
|
## Less structure, more intelligence
|
||||||
|
|
||||||
Prefer simple, readable code over new framework layers and indirection. Add structure only when it removes real complexity, protects an important boundary, or matches an established local pattern. The best fix is often a smaller prompt, a tighter tool contract, a channel-local change, or one focused regression test.
|
Prefer simple, readable code over new framework layers and indirection. Add structure only when it removes real complexity, protects an important boundary, or matches an established local pattern. The best fix is often a smaller prompt, a tighter tool contract, a channel-local change, or one focused regression test.
|
||||||
|
|||||||
@ -45,13 +45,13 @@ from nanobot.security.workspace_access import (
|
|||||||
bind_workspace_scope,
|
bind_workspace_scope,
|
||||||
reset_workspace_scope,
|
reset_workspace_scope,
|
||||||
)
|
)
|
||||||
|
from nanobot.session import turn_continuation
|
||||||
from nanobot.session.goal_state import (
|
from nanobot.session.goal_state import (
|
||||||
goal_state_runtime_lines,
|
goal_state_runtime_lines,
|
||||||
runner_wall_llm_timeout_s,
|
runner_wall_llm_timeout_s,
|
||||||
sustained_goal_active,
|
sustained_goal_active,
|
||||||
)
|
)
|
||||||
from nanobot.session.manager import Session, SessionManager
|
from nanobot.session.manager import Session, SessionManager
|
||||||
from nanobot.session import turn_continuation
|
|
||||||
from nanobot.utils.document import extract_documents, reference_non_image_attachments
|
from nanobot.utils.document import extract_documents, reference_non_image_attachments
|
||||||
from nanobot.utils.helpers import image_placeholder_text
|
from nanobot.utils.helpers import image_placeholder_text
|
||||||
from nanobot.utils.helpers import truncate_text as truncate_text_fn
|
from nanobot.utils.helpers import truncate_text as truncate_text_fn
|
||||||
@ -1017,6 +1017,13 @@ class AgentLoop:
|
|||||||
channel=msg.channel, chat_id=msg.chat_id,
|
channel=msg.channel, chat_id=msg.chat_id,
|
||||||
content="Sorry, I encountered an error.",
|
content="Sorry, I encountered an error.",
|
||||||
))
|
))
|
||||||
|
if not turn_continuation.internal_continuation_pending(msg.metadata):
|
||||||
|
await self._runtime_events().turn_completed(
|
||||||
|
channel=msg.channel,
|
||||||
|
chat_id=msg.chat_id,
|
||||||
|
session_key=session_key,
|
||||||
|
metadata=msg.metadata,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
# Drain any messages still in the pending queue and re-publish
|
# Drain any messages still in the pending queue and re-publish
|
||||||
# them to the bus so they are processed as fresh inbound messages
|
# them to the bus so they are processed as fresh inbound messages
|
||||||
|
|||||||
@ -576,6 +576,45 @@ class TestToolEventProgress:
|
|||||||
assert turn_end_msgs[0].chat_id == "chat1"
|
assert turn_end_msgs[0].chat_id == "chat1"
|
||||||
assert outbound.index(done_msgs[0]) < outbound.index(turn_end_msgs[0])
|
assert outbound.index(done_msgs[0]) < outbound.index(turn_end_msgs[0])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_dispatch_publishes_turn_end_after_error(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
bus = MessageBus()
|
||||||
|
provider = MagicMock()
|
||||||
|
provider.get_default_model.return_value = "test-model"
|
||||||
|
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
|
||||||
|
_attach_webui_runtime_events(loop, bus)
|
||||||
|
|
||||||
|
async def raise_from_turn(*_args, **_kwargs):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
loop._process_message = raise_from_turn # type: ignore[method-assign]
|
||||||
|
|
||||||
|
await loop._dispatch(InboundMessage(
|
||||||
|
channel="websocket",
|
||||||
|
sender_id="u1",
|
||||||
|
chat_id="chat1",
|
||||||
|
content="say hello",
|
||||||
|
))
|
||||||
|
|
||||||
|
outbound = []
|
||||||
|
while bus.outbound_size > 0:
|
||||||
|
outbound.append(await bus.consume_outbound())
|
||||||
|
|
||||||
|
error_msgs = [m for m in outbound if m.content == "Sorry, I encountered an error."]
|
||||||
|
turn_end_msgs = [m for m in outbound if m.metadata.get("_turn_end")]
|
||||||
|
statuses = [m for m in outbound if m.metadata.get("_goal_status")]
|
||||||
|
|
||||||
|
assert len(error_msgs) == 1
|
||||||
|
assert len(turn_end_msgs) == 1
|
||||||
|
assert turn_end_msgs[0].content == ""
|
||||||
|
assert turn_end_msgs[0].chat_id == "chat1"
|
||||||
|
assert [m.metadata["goal_status"] for m in statuses] == ["idle"]
|
||||||
|
assert outbound.index(error_msgs[0]) < outbound.index(turn_end_msgs[0])
|
||||||
|
assert outbound.index(turn_end_msgs[0]) < outbound.index(statuses[-1])
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_webui_title_generation_runs_after_turn_end(self, tmp_path: Path) -> None:
|
async def test_webui_title_generation_runs_after_turn_end(self, tmp_path: Path) -> None:
|
||||||
bus = MessageBus()
|
bus = MessageBus()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user