diff --git a/tests/tui_gateway/test_bot_mode_silence_delivery.py b/tests/tui_gateway/test_bot_mode_silence_delivery.py index d092b95c3d..97e45b962a 100644 --- a/tests/tui_gateway/test_bot_mode_silence_delivery.py +++ b/tests/tui_gateway/test_bot_mode_silence_delivery.py @@ -42,3 +42,30 @@ def test_live_bot_chat_completion_empties_marker_only_for_successful_turns(monke monkeypatch.setattr(srv, "_session_live_title", lambda _s, _k: "Scratch") payload, _, _ = srv._complete_turn_payload(session, _turn({"final_response": "NO_REPLY"}), None, 80) assert payload["text"] == "NO_REPLY" + + +def test_live_bot_chat_stream_holds_back_partial_silence_marker(monkeypatch): + """Mirror of stream_consumer's hold-back: a marker never reaches message.delta, prose that + diverges from every marker is flushed intact once it diverges.""" + events = [] + monkeypatch.setattr(srv, "_emit", lambda event, _sid, payload=None: events.append((event, payload))) + monkeypatch.setattr(srv, "_load_interim_assistant_messages", lambda: False) + monkeypatch.setattr(srv, "_start_usage_ticker", lambda _sid, _agent: (SimpleNamespace(set=lambda: None), SimpleNamespace(join=lambda: None))) + + def _run(final, chunks): + events.clear() + + def run_conversation(_message, **kwargs): + for chunk in chunks: + kwargs["stream_callback"](chunk) + return {"final_response": final} + + agent = SimpleNamespace(_session_title_hint="Bot Chat", run_conversation=run_conversation) + session = {"pending_title": None, "session_key": "k", "history_lock": contextlib.nullcontext(), "agent": agent} + st = srv._TurnRun(agent=agent, one_turn_restore=None, terminal_callback=None, receipt_committed=True) + srv._invoke_agent("sid", session, st, "ping", "ping", None, [], None, None) + return [p["text"] for e, p in events if e == "message.delta"], (session.get("inflight_turn") or {}).get("assistant", "") + + assert _run("NO_REPLY", ["NO_", "REPLY"]) == ([], "") + assert _run("NO way, here is the answer.", ["NO", " way,", " here is the answer."]) == ( + ["NO way,", " here is the answer."], "NO way, here is the answer.") diff --git a/tui_gateway/prompt_turn.py b/tui_gateway/prompt_turn.py index 4a3cfcf392..527f0cdb15 100644 --- a/tui_gateway/prompt_turn.py +++ b/tui_gateway/prompt_turn.py @@ -534,8 +534,19 @@ def _invoke_agent( turn_author: dict | None = None) -> None: """Wire the streaming callbacks and run the conversation into ``st.result``.""" agent = st.agent + # Bot Chat mirrors gateway.stream_consumer: deltas are withheld while the streamed buffer + # could still resolve to a silence marker ("NO"->"NO_REPLY"), so a bare marker is never + # shown and then retracted (the client keeps streamed text when message.complete is ""). + hold = {"buf": "", "held": ""} if _is_bot_mode_session(session) else None def _stream(delta): + if hold is not None and isinstance(delta, str): + from gateway.response_filters import is_partial_silence_marker + hold["buf"] += delta + if is_partial_silence_marker(hold["buf"]): + hold["held"] += delta + return + delta, hold["held"] = hold["held"] + delta, "" with session["history_lock"]: _append_inflight_delta(session, delta) payload = {"text": delta}