mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
- Add runtime/plan_agent_v2 package and shims; gateway/direct_loop/WS wiring - Admin chat: interaction mode and specialist only in user menu; session API stores memory_mode and execution_mode only - POST /admin/api/chat/user-mode mirrors plan_agent_version to AIA_EXPERT_PLAN_AGENT_V2_ENABLED (v2 to 1, v1 to 0) - Composer cleanup (hidden mode select, no reasoning toggle in meta bar); tests and _local/system.env.example Co-authored-by: Cursor <cursoragent@cursor.com>
281 lines
11 KiB
Python
281 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from .manager import PlanModeManagerV2
|
|
from .models import PLAN_MODE_PLAN
|
|
from .prompt_injector import build_plan_mode_prefix, inject_plan_context
|
|
from .trace import emit_plan_agent_v2_trace
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlanAgentV2Decision:
|
|
action: str # enter_plan | stay_plan | run_agent
|
|
reply_text: str
|
|
plan_state: dict[str, Any]
|
|
system_prompt_override: str = ""
|
|
|
|
|
|
def _is_confirm_text(text: str) -> bool:
|
|
t = str(text or "").strip().lower()
|
|
return t in {"确认", "确认计划", "同意", "通过", "approve", "approved", "confirm", "yes"}
|
|
|
|
|
|
def _normalize_user_text(text: str) -> str:
|
|
return " ".join(str(text or "").strip().lower().split())
|
|
|
|
|
|
def _is_low_signal_continue(text_norm: str) -> bool:
|
|
t = str(text_norm or "").strip().lower()
|
|
return t in {
|
|
"继续",
|
|
"继续啊",
|
|
"继续吧",
|
|
"可以",
|
|
"好的",
|
|
"好",
|
|
"ok",
|
|
"okay",
|
|
"go on",
|
|
"continue",
|
|
}
|
|
|
|
|
|
def _confirm_strategy(store: Any) -> str:
|
|
try:
|
|
raw = str(store.get_setting("AIA_EXPERT_PLAN_CONFIRM_STRATEGY") or "").strip().lower()
|
|
except Exception:
|
|
raw = ""
|
|
if raw in {"auto", "strict", "off"}:
|
|
return raw
|
|
return "strict"
|
|
|
|
|
|
def _last_user_text_norm_from_history(*, store: Any, session_id: str) -> str:
|
|
"""Most recent persisted user message (current turn is usually not persisted yet)."""
|
|
try:
|
|
msgs = store.get_messages(session_id=session_id, limit=120)
|
|
except Exception:
|
|
return ""
|
|
for m in reversed(msgs):
|
|
if str(getattr(m, "role", "") or "").strip().lower() == "user":
|
|
return _normalize_user_text(str(getattr(m, "content", "") or ""))
|
|
return ""
|
|
|
|
|
|
def _agent_conversation_stall_suffix(*, lang: str) -> str:
|
|
is_en = str(lang or "").startswith("en")
|
|
if is_en:
|
|
return (
|
|
"\n\n[Conversation stall guard — agent mode]\n"
|
|
"The user's latest message matches their previous user message in this session.\n"
|
|
"- Do not repeat your last assistant reply or restate \"I will now…\" boilerplate.\n"
|
|
"- Make substantive progress: execute the next concrete tool step, produce new actionable output, "
|
|
"or ask exactly one specific blocking question.\n"
|
|
)
|
|
return (
|
|
"\n\n【对话停滞防护 · agent 模式】\n"
|
|
"检测到用户本条输入与上一轮用户输入相同(会话已持久化部分)。\n"
|
|
"- 禁止复述上一轮助手回复或重复「接下来我将…」式独白。\n"
|
|
"- 必须给出实质进展:执行具体工具步骤、写出新的可执行结果,或只提一个关键追问。\n"
|
|
)
|
|
|
|
|
|
def evaluate_for_expert_mode(
|
|
*,
|
|
store: Any,
|
|
session_id: str,
|
|
lang: str,
|
|
requested_specialist: str,
|
|
user_text: str,
|
|
execution_mode: str = "agent",
|
|
base_system_prompt: str,
|
|
trace_id: str | None = None,
|
|
parent_span_id: str | None = None,
|
|
) -> PlanAgentV2Decision:
|
|
mgr = PlanModeManagerV2(store=store)
|
|
st = mgr.load_state(session_id=session_id)
|
|
txt = str(user_text or "").strip()
|
|
txt_norm = _normalize_user_text(txt)
|
|
exec_mode = str(execution_mode or "").strip().lower()
|
|
if exec_mode not in {"agent", "plan"}:
|
|
exec_mode = "plan"
|
|
confirm_strategy = _confirm_strategy(store)
|
|
|
|
if exec_mode == "agent" and st.mode != PLAN_MODE_PLAN:
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_bypassed",
|
|
payload={"requested_mode": "agent", "plan_mode_state": str(st.mode or "")},
|
|
)
|
|
last_user_norm = _last_user_text_norm_from_history(store=store, session_id=session_id)
|
|
stall = bool(txt_norm and last_user_norm and txt_norm == last_user_norm)
|
|
override = ""
|
|
if stall:
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="agent_mode_conversation_stall",
|
|
payload={"reason": "repeated_user_message"},
|
|
)
|
|
base = str(base_system_prompt or "").strip()
|
|
suffix = _agent_conversation_stall_suffix(lang=lang).strip()
|
|
override = f"{base}\n\n{suffix}".strip()
|
|
return PlanAgentV2Decision(
|
|
action="run_agent",
|
|
reply_text="",
|
|
plan_state=st.to_dict(),
|
|
system_prompt_override=override,
|
|
)
|
|
|
|
if st.mode != PLAN_MODE_PLAN:
|
|
entered = mgr.enter(session_id=session_id, owner_specialist=requested_specialist, force_new_plan=False)
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_entered",
|
|
payload={"owner_specialist": entered.owner_specialist, "plan_id": entered.plan_id},
|
|
)
|
|
prefix = build_plan_mode_prefix(state=entered, lang=lang)
|
|
return PlanAgentV2Decision(
|
|
action="run_agent",
|
|
reply_text="",
|
|
plan_state=entered.to_dict(),
|
|
system_prompt_override=f"{prefix}\n\n{str(base_system_prompt or '').strip()}".strip(),
|
|
)
|
|
|
|
st = mgr.refresh_plan_content(session_id=session_id)
|
|
st = mgr.update_loop_guard(session_id=session_id, user_text_norm=txt_norm)
|
|
if _is_confirm_text(txt):
|
|
if exec_mode != "agent" and confirm_strategy == "strict":
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_confirm_blocked",
|
|
payload={
|
|
"reason": "execution_mode_not_agent",
|
|
"requested_mode": exec_mode,
|
|
"confirm_strategy": confirm_strategy,
|
|
},
|
|
)
|
|
blocked_reply = (
|
|
"Plan is ready. Please switch to agent mode, then confirm to execute."
|
|
if str(lang or "").startswith("en")
|
|
else "计划已就绪。请先切换到 agent 模式,再回复“确认”开始执行。"
|
|
)
|
|
return PlanAgentV2Decision(
|
|
action="stay_plan",
|
|
reply_text=blocked_reply,
|
|
plan_state=st.to_dict(),
|
|
system_prompt_override="",
|
|
)
|
|
if exec_mode != "agent" and confirm_strategy == "auto":
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_confirm_auto_switched",
|
|
payload={"from_mode": exec_mode, "to_mode": "agent", "confirm_strategy": confirm_strategy},
|
|
)
|
|
confirmed = mgr.confirm(session_id=session_id)
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_confirmed",
|
|
payload={
|
|
"plan_id": confirmed.plan_id,
|
|
"plan_confirmed": bool(confirmed.plan_confirmed),
|
|
"confirm_strategy": confirm_strategy,
|
|
},
|
|
)
|
|
next_system = inject_plan_context(base_system=base_system_prompt, state=confirmed, lang=lang)
|
|
reply = mgr.build_approved_execution_message(state=confirmed, lang=lang)
|
|
return PlanAgentV2Decision(
|
|
action="run_agent",
|
|
reply_text=reply,
|
|
plan_state=confirmed.to_dict(),
|
|
system_prompt_override=next_system,
|
|
)
|
|
|
|
if _is_low_signal_continue(txt_norm):
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_active",
|
|
payload={"plan_id": st.plan_id, "plan_path": st.plan_path, "loop_guard": "low_signal_continue"},
|
|
)
|
|
low_signal_reply = (
|
|
"Plan mode detected a low-information continuation. "
|
|
"Please provide concrete plan adjustments, or switch to agent mode and reply 'confirm' to execute."
|
|
if str(lang or "").startswith("en")
|
|
else "检测到低信息续写(如“继续/可以”)。请给出具体计划修改点,或切换到 agent 模式后回复“确认”直接执行。"
|
|
)
|
|
return PlanAgentV2Decision(
|
|
action="stay_plan",
|
|
reply_text=low_signal_reply,
|
|
plan_state=st.to_dict(),
|
|
system_prompt_override="",
|
|
)
|
|
|
|
if int(st.plan_loop_count or 0) >= 2:
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_active",
|
|
payload={"plan_id": st.plan_id, "plan_path": st.plan_path, "loop_guard": "hard_block"},
|
|
)
|
|
anti_loop_reply = (
|
|
"I am in plan mode. I will only output a concise executable plan. "
|
|
"If you want me to execute, switch to agent mode and reply 'confirm'."
|
|
if str(lang or "").startswith("en")
|
|
else "当前为 plan 模式,我只输出可执行计划。若要开始执行,请切换到 agent 模式并回复“确认”。"
|
|
)
|
|
return PlanAgentV2Decision(
|
|
action="stay_plan",
|
|
reply_text=anti_loop_reply,
|
|
plan_state=st.to_dict(),
|
|
system_prompt_override="",
|
|
)
|
|
|
|
prefix = build_plan_mode_prefix(state=st, lang=lang)
|
|
anti_loop_suffix = (
|
|
"\n\n[Anti-loop guard]\n"
|
|
"- Do not repeat the previous response.\n"
|
|
"- If user asks similarly, refine with more concrete steps, checks, and fallback.\n"
|
|
"- Keep output as plan only; do not pretend execution is complete."
|
|
)
|
|
emit_plan_agent_v2_trace(
|
|
store=store,
|
|
session_id=session_id,
|
|
trace_id=trace_id,
|
|
parent_span_id=parent_span_id,
|
|
event_type="plan_mode_active",
|
|
payload={"plan_id": st.plan_id, "plan_path": st.plan_path, "loop_count": int(st.plan_loop_count or 0)},
|
|
)
|
|
return PlanAgentV2Decision(
|
|
action="run_agent",
|
|
reply_text="",
|
|
plan_state=st.to_dict(),
|
|
system_prompt_override=f"{prefix}{anti_loop_suffix}\n\n{str(base_system_prompt or '').strip()}".strip(),
|
|
)
|
|
|
|
|
|
__all__ = ["PlanAgentV2Decision", "evaluate_for_expert_mode"]
|
|
|