agent-Specialization/server/goal_flow.py
JOJO 17c49f6258 fix(goal): 目标状态改为对话级存储,修复多对话目标串扰与进度事件串台
- goal_state.json(工作区级单实例) → goal_states/<conversation_id>.json(对话级)
- 移除 start_conversation_id 冗余字段与“工作区唯一目标”“压缩改id”等过时注释
- goal 进度/审核事件快照补 conversation_id,前端现有过滤逻辑自动生效
- 清理 tasks/media.py、tasks/helpers.py 未使用的残留 import
2026-08-19 13:53:51 +08:00

275 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""目标模式Goal Mode在 Web 任务主循环中的编排逻辑。
目标状态是对话级的(见 modules/goal_state_manager.py每个对话独立持有
自己的目标多对话并发互不影响。本模块将启动、提示词注入、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: str,
goal_text: str,
current_tool_calls: int,
) -> bool:
"""为本对话启动一个新目标。
目标状态是对话级的启动会覆盖本对话自己的旧目标状态running/done/stopped、
review_history、token 基线、开始时间等),不影响其他对话的目标。
"""
if not conversation_id:
return False
gsm = GoalStateManager(workspace.data_dir, conversation_id)
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,
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, conversation_id: Optional[str]) -> bool:
if not conversation_id:
return False
try:
return GoalStateManager(workspace.data_dir, conversation_id).is_active()
except ValueError:
return False
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]],
conversation_id: Optional[str],
total_tool_calls: int = 0,
extra: Optional[Dict[str, Any]] = None,
) -> None:
"""广播本对话的目标模式进度(快照自带 conversation_id前端按对话过滤
用于刷新后恢复与进度弹窗动态数字。"""
if not conversation_id:
return
try:
gsm = GoalStateManager(workspace.data_dir, conversation_id)
except ValueError:
return
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 回主循环开下一轮。
"""
if not conversation_id:
return {"action": "inactive"}
try:
gsm = GoalStateManager(workspace.data_dir, conversation_id)
except ValueError:
return {"action": "inactive"}
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", {"conversation_id": conversation_id, "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", {"conversation_id": conversation_id, "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, conversation_id: Optional[str], total_tool_calls: int = 0) -> bool:
"""用户停止任务时,若本对话有活动目标则一并停止。返回是否有活动目标被停止。"""
if not conversation_id:
return False
try:
gsm = GoalStateManager(workspace.data_dir, conversation_id)
except ValueError:
return False
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