252 lines
9.1 KiB
Python
252 lines
9.1 KiB
Python
"""目标模式(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
|