fix(webui): hide subagent backfill payloads

This commit is contained in:
chengyongru
2026-07-02 14:36:23 +08:00
committed by Xubin Ren
parent 5abe06f808
commit 34535b4e7c
11 changed files with 232 additions and 12 deletions
+62
View File
@@ -465,6 +465,68 @@ async def test_loop_injected_followup_preserves_image_media(tmp_path):
)
@pytest.mark.asyncio
async def test_subagent_pending_injection_is_hidden_history_and_not_merged(tmp_path):
from nanobot.agent.loop import AgentLoop
from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.session.history_visibility import HIDDEN_HISTORY_META
bus = MessageBus()
provider = MagicMock()
provider.get_default_model.return_value = "test-model"
call_count = {"n": 0}
async def chat_with_retry(*, messages, **kwargs):
call_count["n"] += 1
if call_count["n"] == 1:
return LLMResponse(content="first answer", tool_calls=[], usage={})
return LLMResponse(content="second answer", tool_calls=[], usage={})
provider.chat_with_retry = chat_with_retry
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path, model="test-model")
loop.tools.get_definitions = MagicMock(return_value=[])
payload = (
"[Subagent 'x' completed successfully]\n\n"
"Task: t\n\n"
"Result:\nr\n\n"
"Summarize this naturally for the user."
)
pending_queue = asyncio.Queue()
await pending_queue.put(InboundMessage(
channel="cli",
sender_id="user",
chat_id="c",
content="visible follow-up",
))
await pending_queue.put(InboundMessage(
channel="system",
sender_id="subagent",
chat_id="cli:c",
content=payload,
metadata={"injected_event": "subagent_result", "subagent_task_id": "sub-1"},
))
final_content, _, all_msgs, _, had_injections = await loop._run_agent_loop(
[{"role": "user", "content": "hello"}],
channel="cli",
chat_id="c",
pending_queue=pending_queue,
)
assert final_content == "second answer"
assert had_injections is True
assert call_count["n"] == 2
injected_users = [message for message in all_msgs if message.get("role") == "user"][-2:]
assert [message["content"] for message in injected_users] == ["visible follow-up", payload]
assert injected_users[1][HIDDEN_HISTORY_META] == {
"kind": "subagent_result",
"subagent_task_id": "sub-1",
}
assert injected_users[1]["injected_event"] == "subagent_result"
@pytest.mark.asyncio
async def test_runner_merges_multiple_injected_user_messages_without_losing_media():
"""Multiple injected follow-ups should not create lossy consecutive user messages."""
+46
View File
@@ -3021,3 +3021,49 @@ def test_handle_webui_thread_get_does_not_backfill_trigger_internal_prompt(
body = json.loads(resp.body.decode())
assert [message["role"] for message in body["messages"]] == ["assistant"]
assert [message["content"] for message in body["messages"]] == ["PR #4502 已经开始 review。"]
def test_handle_webui_thread_get_does_not_backfill_hidden_subagent_result(
tmp_path,
monkeypatch,
) -> None:
from urllib.parse import quote
from websockets.datastructures import Headers
from websockets.http11 import Request
from nanobot.session.history_visibility import HIDDEN_HISTORY_META
from nanobot.webui.transcript import append_transcript_object
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
workspace = tmp_path / "workspace"
sessions = SessionManager(workspace)
key = "websocket:c-subagent"
session = sessions.get_or_create(key)
session.add_message(
"user",
"internal subagent result",
**{HIDDEN_HISTORY_META: {"kind": "subagent_result", "subagent_task_id": "sub-1"}},
)
session.add_message("assistant", "subagent summary")
sessions.save(session)
append_transcript_object(
key,
{"event": "message", "chat_id": "c-subagent", "text": "subagent summary"},
)
bus = MagicMock()
channel = WebSocketChannel(
{"enabled": True, "allowFrom": ["*"]},
bus,
gateway=_basic_handler(bus, session_manager=sessions, workspace_path=workspace),
)
channel.gateway.tokens.api_tokens["tok"] = time.monotonic() + 300.0
enc = quote(key, safe="")
req = Request(f"/api/sessions/{enc}/webui-thread", Headers([("Authorization", "Bearer tok")]))
resp = channel.gateway.http._handle_webui_thread_get(req, enc)
assert resp.status_code == 200
body = json.loads(resp.body.decode())
assert [message["role"] for message in body["messages"]] == ["assistant"]
assert [message["content"] for message in body["messages"]] == ["subagent summary"]
+42
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from nanobot.session.history_visibility import HIDDEN_HISTORY_META
from nanobot.webui.transcript import (
WEBUI_TRANSCRIPT_SCHEMA_VERSION,
append_fork_marker,
@@ -698,6 +699,47 @@ def test_backfill_does_not_misalign_when_session_only_has_transcript_tail(
]
def test_backfill_skips_internal_subagent_results(tmp_path, monkeypatch) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
key = "websocket:t-subagent"
for ev in (
{"event": "message", "chat_id": "t-subagent", "text": "summary one"},
{"event": "turn_end", "chat_id": "t-subagent"},
{"event": "message", "chat_id": "t-subagent", "text": "summary two"},
{"event": "turn_end", "chat_id": "t-subagent"},
):
append_transcript_object(key, ev)
legacy_raw = (
"[Subagent 'legacy' completed successfully]\n\n"
"Task: t\n\n"
"Result:\nr\n\n"
"Summarize this naturally for the user."
)
out = build_webui_thread_response(
key,
session_messages=[
{"role": "user", "content": legacy_raw},
{"role": "assistant", "content": "summary one"},
{
"role": "user",
"content": "marked result",
HIDDEN_HISTORY_META: {
"kind": "subagent_result",
"subagent_task_id": "sub-1",
},
},
{"role": "assistant", "content": "summary two"},
],
)
assert out is not None
assert [(message["role"], message["content"]) for message in out["messages"]] == [
("assistant", "summary one"),
("assistant", "summary two"),
]
def test_replay_infers_video_media_from_attachment_name() -> None:
msgs = replay_transcript_to_ui_messages(
[
+15
View File
@@ -7,6 +7,7 @@ from pathlib import Path
import nanobot.webui.session_list_index as session_list_index
from nanobot.cron.session_turns import CRON_HISTORY_META
from nanobot.session.automation_turns import AUTOMATION_HISTORY_META
from nanobot.session.history_visibility import HIDDEN_HISTORY_META
from nanobot.session.manager import SessionManager
@@ -103,6 +104,20 @@ def test_webui_session_list_skips_trigger_internal_user_preview(tmp_path: Path)
assert list_webui_sessions(manager)[0]["preview"] == "PR #4502 已经开始 review。"
def test_webui_session_list_skips_hidden_history_user_preview(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:hidden-preview")
session.add_message(
"user",
"internal subagent result",
**{HIDDEN_HISTORY_META: {"kind": "subagent_result", "subagent_task_id": "sub-1"}},
)
session.add_message("assistant", "subagent summary")
manager.save(session)
assert list_webui_sessions(manager)[0]["preview"] == "subagent summary"
def test_webui_session_list_uses_webui_transcript_activity_for_sort(
tmp_path: Path,
monkeypatch,