"""目标模式(Goal Mode)在 Web 任务主循环中的编排逻辑。 将启动、提示词注入、turn 结束后的审核/续命/停止判定集中在此, 尽量减少对 chat_flow_task_main.py 的侵入。 """ from __future__ import annotations from typing import Any, Callable, Dict, Optional from modules.goal_state_manager import ( GoalStateManager, REASON_IDLE_NO_TOOL, REASON_MAX_TOKENS, REASON_MAX_TURNS, REASON_USER_CANCEL, ) from modules.goal_review_agent import GoalReviewAgent from modules.personalization_manager import load_personalization_config from .chat_flow_task_support import inject_runtime_user_message # ① 注入的目标模式提示词(开头一次 + 每次压缩后重注入) GOAL_MODE_PROMPT = ( "【目标模式已开启】以上是本次的目标。在目标达成前,请持续推进," "不要把控制权交还给我、也不要停下来等我确认。请用实际的工具操作推进工作," "避免只停留在计划或反复询问。每当你认为工作告一段落停下时,会有独立的审核方" "对照目标检查是否真正达成;若未达成,你会收到具体的下一步指示并继续。" ) # 续命消息前缀 CONTINUE_PREFIX = "审核智能体对于你的工作结束给出了以下内容:" def _load_token_total(web_terminal) -> int: """读取工作区累计 total_tokens。""" try: totals = web_terminal.context_manager._load_token_totals() return int(totals.get("total_tokens") or 0) except Exception: return 0 def snapshot_token_baseline(web_terminal) -> Dict[str, int]: try: totals = web_terminal.context_manager._load_token_totals() return { "input_tokens": int(totals.get("input_tokens") or 0), "output_tokens": int(totals.get("output_tokens") or 0), "total_tokens": int(totals.get("total_tokens") or 0), } except Exception: return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} def maybe_start_goal( *, web_terminal, workspace, conversation_id: Optional[str], goal_text: str, current_tool_calls: int, ) -> bool: """启动一个新目标。 本函数语义是“本次用户显式开启目标模式”而不是“续接已有目标”。 因此必须覆盖工作区级 goal_state.json 中的旧目标状态(包括 running/done/stopped、 review_history、token 基线、开始时间等),避免新对话复用上一轮目标。 """ gsm = GoalStateManager(workspace.data_dir) cfg = load_personalization_config(workspace.data_dir) review_mode = cfg.get("goal_review_mode") or "readonly" end_conditions = cfg.get("goal_end_conditions") or ["max_turns"] max_turns = cfg.get("goal_max_turns") if "max_turns" in end_conditions else None max_tokens = cfg.get("goal_max_tokens") if "max_tokens" in end_conditions else None gsm.start( goal=goal_text, review_mode=review_mode, max_turns=max_turns, max_tokens=max_tokens, conversation_id=conversation_id, token_baseline=snapshot_token_baseline(web_terminal), tool_call_baseline=current_tool_calls, ) return True def inject_goal_prompt( *, web_terminal, messages, sender: Optional[Callable[[str, Dict[str, Any]], None]], conversation_id: Optional[str], ) -> None: """注入 ① 号目标模式提示词(开头 / 压缩后重注入共用)。""" inject_runtime_user_message( web_terminal=web_terminal, messages=messages, text=GOAL_MODE_PROMPT, source="goal_prompt", sender=sender, conversation_id=conversation_id, inline=False, persist=True, ) def goal_is_active(workspace) -> bool: return GoalStateManager(workspace.data_dir).is_active() def _emit_snapshot(sender, event: str, gsm: GoalStateManager, *, total_tokens: int, tool_calls: int, extra: Optional[Dict] = None): if not callable(sender): return snap = gsm.progress_snapshot(current_total_tokens=total_tokens, current_tool_calls=tool_calls) if extra: snap.update(extra) try: sender(event, snap) except Exception: pass def emit_goal_progress( *, web_terminal, workspace, sender: Optional[Callable[[str, Dict[str, Any]], None]], total_tool_calls: int = 0, extra: Optional[Dict[str, Any]] = None, ) -> None: """向前端广播当前目标模式进度,用于刷新后恢复与进度弹窗动态数字。""" gsm = GoalStateManager(workspace.data_dir) if not gsm.state: return total_tokens = _load_token_total(web_terminal) _emit_snapshot( sender, "goal_progress", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls, extra=extra, ) async def handle_goal_after_turn( *, web_terminal, workspace, messages, sender: Optional[Callable[[str, Dict[str, Any]], None]], conversation_id: Optional[str], assistant_content: str, made_tool_call: bool, total_tool_calls: int, ) -> Dict[str, Any]: """主模型某轮结束(无 tool_calls)后的目标处理。 返回 {'action': 'inactive'|'stop'|'done'|'continue', ...}: - inactive:当前无活动目标,调用方照常结束本次任务。 - stop:目标因空转/边界停止,调用方照常结束(事件已发)。 - done:目标达成,调用方照常结束(事件已发)。 - continue:已注入续命消息,调用方应 continue 回主循环开下一轮。 """ gsm = GoalStateManager(workspace.data_dir) if not gsm.is_active(): return {"action": "inactive"} total_tokens = _load_token_total(web_terminal) _emit_snapshot(sender, "goal_progress", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls) # 1) 空转保护:本目标轮主模型一次工具都没调过 → 直接停止 if not made_tool_call: gsm.mark_stopped(REASON_IDLE_NO_TOOL) _emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls) return {"action": "stop", "reason": REASON_IDLE_NO_TOOL} # 2) 计入一轮,检查结束边界 gsm.increment_turn() _emit_snapshot(sender, "goal_progress", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls) if gsm.reached_max_turns(): gsm.mark_stopped(REASON_MAX_TURNS) _emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls) return {"action": "stop", "reason": REASON_MAX_TURNS} if gsm.reached_max_tokens(total_tokens): gsm.mark_stopped(REASON_MAX_TOKENS) _emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls) return {"action": "stop", "reason": REASON_MAX_TOKENS} # 3) 调用审核智能体 payload_text = gsm.build_review_payload_text(assistant_content) if callable(sender): try: sender("goal_review_progress", {"progress": {"stage": "start", "message": "开始审核"}}) except Exception: pass try: agent = GoalReviewAgent(web_terminal=web_terminal) def _progress(progress: Dict[str, Any]) -> None: if callable(sender): try: sender("goal_review_progress", {"progress": progress}) except Exception: pass result = await agent.review( payload_text=payload_text, review_mode=gsm.get_review_mode(), progress_cb=_progress, ) except Exception as exc: # 审核异常 → 保守续命,但仍受边界约束 result = {"status": "continue", "message": f"目标审核出现异常({exc}),请继续核对目标并推进未完成的部分。"} status = (result or {}).get("status") message = (result or {}).get("message") or "" if status == "done": gsm.mark_done(message) _emit_snapshot( sender, "goal_completed", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls, extra={"summary": message}, ) return {"action": "done"} # continue:记录审核历史 + 注入续命消息 gsm.append_review(assistant_content, message) inject_runtime_user_message( web_terminal=web_terminal, messages=messages, text=f"{CONTINUE_PREFIX}{message}", source="goal_review", sender=sender, conversation_id=conversation_id, inline=False, persist=True, ) return {"action": "continue", "message": message} def stop_goal_user_cancel(*, web_terminal, workspace, sender, total_tool_calls: int = 0) -> bool: """用户停止任务时,若有活动目标则一并停止。返回是否有活动目标被停止。""" gsm = GoalStateManager(workspace.data_dir) if not gsm.is_active(): return False total_tokens = _load_token_total(web_terminal) gsm.mark_stopped(REASON_USER_CANCEL) _emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls) return True