diff --git a/pytests/test_maisaka_timing_gate.py b/pytests/test_maisaka_timing_gate.py index 02b314da..a2f79b8a 100644 --- a/pytests/test_maisaka_timing_gate.py +++ b/pytests/test_maisaka_timing_gate.py @@ -1,6 +1,7 @@ from datetime import datetime from types import SimpleNamespace +import asyncio import pytest from src.core.tooling import ToolExecutionResult, ToolInvocation @@ -8,6 +9,7 @@ from src.llm_models.payload_content.tool_option import ToolCall from src.maisaka.chat_loop_service import ChatResponse, MaisakaChatLoopService from src.maisaka.context_messages import AssistantMessage, TIMING_GATE_INVALID_TOOL_HINT_SOURCE from src.maisaka.reasoning_engine import MaisakaReasoningEngine +from src.maisaka.runtime import MaisakaHeartFlowChatting def _build_chat_response(tool_calls: list[ToolCall]) -> ChatResponse: @@ -173,6 +175,29 @@ def test_timing_gate_invalid_tool_hint_only_visible_to_timing_gate() -> None: assert planner_history == [] +def test_forced_timing_trigger_bypasses_message_frequency_threshold() -> None: + runtime = SimpleNamespace( + _STATE_WAIT="wait", + _agent_state="stop", + _message_turn_scheduled=False, + _internal_turn_queue=asyncio.Queue(), + _has_pending_messages=lambda: True, + _get_pending_message_count=lambda: 1, + _has_forced_timing_trigger=lambda: True, + _cancel_deferred_message_turn_task=lambda: None, + ) + + def _fail_get_message_trigger_threshold() -> int: + raise AssertionError("@/提及必回不应被普通聊天频率阈值拦住") + + runtime._get_message_trigger_threshold = _fail_get_message_trigger_threshold + + MaisakaHeartFlowChatting._schedule_message_turn(runtime) # type: ignore[arg-type] + + assert runtime._message_turn_scheduled is True + assert runtime._internal_turn_queue.get_nowait() == "message" + + def test_finish_tool_is_not_written_back_to_history() -> None: finish_call = ToolCall(call_id="finish-call", func_name="finish", args={}) reply_call = ToolCall(call_id="reply-call", func_name="reply", args={}) diff --git a/src/maisaka/runtime.py b/src/maisaka/runtime.py index a96eaf11..12e9fcf5 100644 --- a/src/maisaka/runtime.py +++ b/src/maisaka/runtime.py @@ -473,13 +473,18 @@ class MaisakaHeartFlowChatting: def _update_message_trigger_state(self, message: SessionMessage) -> None: """补齐消息中的 @/提及 标记,并在命中时启用强制 continue。""" - detected_mentioned, detected_at, _ = is_mentioned_bot_in_message(message) + detected_mentioned, detected_at, reply_probability_boost = is_mentioned_bot_in_message(message) if detected_at: message.is_at = True if detected_mentioned: message.is_mentioned = True - if not message.is_at and not message.is_mentioned: + should_force_reply = ( + reply_probability_boost >= 1.0 + or (message.is_at and global_config.chat.at_bot_inevitable_reply) + or (message.is_mentioned and global_config.chat.mentioned_bot_reply) + ) + if not should_force_reply or (not message.is_at and not message.is_mentioned): return self._arm_force_next_timing_continue( @@ -537,6 +542,11 @@ class MaisakaHeartFlowChatting: self._force_next_timing_reason = "" return reason + def _has_forced_timing_trigger(self) -> bool: + """判断是否已有 @/提及必回触发,需绕过普通频率阈值。""" + + return self._force_next_timing_continue + def _bind_planner_interrupt_flag(self, interrupt_flag: asyncio.Event) -> None: """绑定当前可打断请求使用的中断标记。""" self._planner_interrupt_flag = interrupt_flag @@ -867,6 +877,12 @@ class MaisakaHeartFlowChatting: if pending_count <= 0: return + if self._has_forced_timing_trigger(): + self._cancel_deferred_message_turn_task() + self._message_turn_scheduled = True + self._internal_turn_queue.put_nowait("message") + return + trigger_threshold = self._get_message_trigger_threshold() if pending_count >= trigger_threshold or self._should_trigger_message_turn_by_idle_compensation( pending_count=pending_count,