From e74906fbec3e2d1c090bfba71bfdfff41c1400fa Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 15 Jun 2026 20:33:52 +0800 Subject: [PATCH] fix(evolution): skip idle review while a turn is running --- agent/chat/service.py | 15 +++++++++++++++ agent/evolution/prompts.py | 17 +++++++++++++++++ agent/evolution/trigger.py | 18 ++++++++++++++++++ bridge/agent_bridge.py | 16 ++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/agent/chat/service.py b/agent/chat/service.py index ca8f8647..66e91792 100644 --- a/agent/chat/service.py +++ b/agent/chat/service.py @@ -55,6 +55,10 @@ class ChatService: context = self._build_context(query, session_id, channel_type) self._attach_context_aware_tools(agent, context) + # Mark this session as mid-run so the self-evolution idle scan does not + # fire concurrently when a single turn runs longer than idle_minutes. + self._mark_run_active(agent, True) + # State shared between the event callback and this method state = _StreamState() @@ -205,6 +209,8 @@ class ChatService: logger.info("[ChatService] Cleared agent message history after executor recovery") raise finally: + # Clear the mid-run flag so idle scans can review this session again. + self._mark_run_active(agent, False) # Release cancel token to keep the registry bounded. if session_id: try: @@ -314,6 +320,15 @@ class ChatService: except Exception as e: logger.warning(f"[ChatService] Failed to attach context to scheduler: {e}") + @staticmethod + def _mark_run_active(agent, active): + """Toggle the self-evolution mid-run flag for this session's agent.""" + try: + from agent.evolution.trigger import mark_run_active + mark_run_active(agent, active) + except Exception: + pass + @staticmethod def _note_evolution_turn(agent, context): """Record a user turn so the self-evolution idle trigger has signal.""" diff --git a/agent/evolution/prompts.py b/agent/evolution/prompts.py index 66b068bb..4fc7d4dd 100644 --- a/agent/evolution/prompts.py +++ b/agent/evolution/prompts.py @@ -164,7 +164,24 @@ def build_review_user_message(transcript: str, protected_skills: list = None) -> "rare last resorts — stay [SILENT] unless there is a clear, durable signal " "not already covered." f"{protected_note}\n" + f"{_language_instruction()}\n" "\n" f"{transcript}\n" "" ) + + +def _language_instruction() -> str: + """Force the user-facing summary into the configured UI language. + + The summary must match what the user reads, so resolve the language from + i18n rather than letting the model guess from the (English-heavy) prompt. + """ + try: + from common import i18n + lang_name = "中文" if i18n.is_zh() else "English" + except Exception: + lang_name = "the user's language" + return ( + f"\nIMPORTANT: if you produce a summary, write it ENTIRELY in {lang_name}. " + ) diff --git a/agent/evolution/trigger.py b/agent/evolution/trigger.py index 8055cbae..e044339c 100644 --- a/agent/evolution/trigger.py +++ b/agent/evolution/trigger.py @@ -73,6 +73,20 @@ def note_user_turn(agent, channel_type: str = "", receiver: str = "") -> None: pass +def mark_run_active(agent, active: bool) -> None: + """Flag whether the agent is mid-run, so idle scans skip a busy session. + + Without this, a single run that lasts longer than idle_minutes would let + the scanner fire an evolution pass concurrently with the live turn. + """ + try: + agent._evo_run_active = bool(active) + if active: + agent._evo_last_active = time.time() + except Exception: + pass + + def start_evolution_trigger(agent_bridge) -> None: """Start the idle-scan thread once per process (idempotent).""" if getattr(agent_bridge, "_evolution_trigger_started", False): @@ -105,6 +119,10 @@ def _scan_once(agent_bridge, cfg) -> None: sessions = list(getattr(agent_bridge, "agents", {}).items()) for session_id, agent in sessions: try: + # Skip sessions whose agent is mid-run: a long turn must not be + # reviewed while it is still producing the answer. + if getattr(agent, "_evo_run_active", False): + continue last_active = getattr(agent, "_evo_last_active", 0) turns = int(getattr(agent, "_evo_turns", 0)) # Enough signal = enough turns OR enough context pressure. diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index fc8c2756..e0b54f88 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -524,6 +524,15 @@ class AgentBridge: session_id, query, context, clear_history ) + # Mark this session as mid-run so the self-evolution idle scan does + # not fire concurrently when a single turn runs longer than + # idle_minutes. + try: + from agent.evolution.trigger import mark_run_active + mark_run_active(agent, True) + except Exception: + pass + try: # Use agent's run_stream method with event handler response = agent.run_stream( @@ -533,6 +542,13 @@ class AgentBridge: cancel_event=cancel_event, ) finally: + # Clear the mid-run flag so idle scans can review this session. + try: + from agent.evolution.trigger import mark_run_active + mark_run_active(agent, False) + except Exception: + pass + # Restore original tools if context and context.get("is_scheduled_task"): agent.tools = original_tools