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>
185 lines
6.5 KiB
Python
185 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .models import PLAN_MODE_NORMAL, PLAN_MODE_PLAN, PlanAgentStateV2
|
|
from .state_store import PlanAgentStateStoreV2
|
|
|
|
|
|
def _default_plan_dir() -> Path:
|
|
return Path(__file__).resolve().parents[2] / "data" / "plans"
|
|
|
|
|
|
def _resolve_plan_dir(store: Any) -> Path:
|
|
raw = str(store.get_setting("AIA_EXPERT_PLAN_FILE_DIR") or "").strip()
|
|
if raw:
|
|
p = Path(raw)
|
|
return p if p.is_absolute() else (Path(__file__).resolve().parents[2] / p)
|
|
return _default_plan_dir()
|
|
|
|
|
|
def _plan_template() -> str:
|
|
return (
|
|
"# Plan\n\n"
|
|
"## Goal\n"
|
|
"- \n\n"
|
|
"## Scope\n"
|
|
"- \n\n"
|
|
"## Steps\n"
|
|
"1. \n"
|
|
"2. \n"
|
|
"3. \n\n"
|
|
"## Risks\n"
|
|
"- \n\n"
|
|
"## Acceptance\n"
|
|
"- \n"
|
|
)
|
|
|
|
|
|
class PlanModeManagerV2:
|
|
def __init__(self, *, store: Any):
|
|
self._store = store
|
|
self._state_store = PlanAgentStateStoreV2(store)
|
|
|
|
def load_state(self, *, session_id: str) -> PlanAgentStateV2:
|
|
return self._state_store.load(session_id=session_id)
|
|
|
|
def enter(
|
|
self,
|
|
*,
|
|
session_id: str,
|
|
owner_specialist: str,
|
|
force_new_plan: bool = False,
|
|
) -> PlanAgentStateV2:
|
|
prev = self._state_store.load(session_id=session_id)
|
|
if prev.mode == PLAN_MODE_PLAN and not force_new_plan:
|
|
return prev
|
|
sid = str(session_id or "").strip()
|
|
if not sid:
|
|
return prev
|
|
plan_id = uuid.uuid4().hex
|
|
plan_root = _resolve_plan_dir(self._store) / sid
|
|
plan_root.mkdir(parents=True, exist_ok=True)
|
|
plan_path = plan_root / f"{plan_id}.md"
|
|
plan_content = _plan_template()
|
|
plan_path.write_text(plan_content, encoding="utf-8")
|
|
now_ms = int(time.time() * 1000)
|
|
next_state = PlanAgentStateV2(
|
|
mode=PLAN_MODE_PLAN,
|
|
owner_specialist=str(owner_specialist or "generalist").strip().lower() or "generalist",
|
|
plan_id=plan_id,
|
|
plan_path=str(plan_path),
|
|
plan_content=plan_content,
|
|
plan_confirmed=False,
|
|
entered_at_ms=now_ms,
|
|
updated_at_ms=now_ms,
|
|
last_user_text_norm="",
|
|
plan_loop_count=0,
|
|
)
|
|
return self._state_store.save(session_id=sid, state=next_state)
|
|
|
|
def refresh_plan_content(self, *, session_id: str) -> PlanAgentStateV2:
|
|
st = self._state_store.load(session_id=session_id)
|
|
p = Path(str(st.plan_path or "").strip())
|
|
if not p.exists() or not p.is_file():
|
|
return st
|
|
content = p.read_text(encoding="utf-8", errors="replace")
|
|
return self._state_store.save(
|
|
session_id=session_id,
|
|
state=PlanAgentStateV2(
|
|
mode=st.mode,
|
|
owner_specialist=st.owner_specialist,
|
|
plan_id=st.plan_id,
|
|
plan_path=st.plan_path,
|
|
plan_content=content,
|
|
plan_confirmed=st.plan_confirmed,
|
|
entered_at_ms=st.entered_at_ms,
|
|
updated_at_ms=st.updated_at_ms,
|
|
last_user_text_norm=st.last_user_text_norm,
|
|
plan_loop_count=st.plan_loop_count,
|
|
),
|
|
)
|
|
|
|
def update_loop_guard(self, *, session_id: str, user_text_norm: str) -> PlanAgentStateV2:
|
|
st = self._state_store.load(session_id=session_id)
|
|
nxt_count = int(st.plan_loop_count or 0) + 1 if user_text_norm and user_text_norm == st.last_user_text_norm else 0
|
|
return self._state_store.save(
|
|
session_id=session_id,
|
|
state=PlanAgentStateV2(
|
|
mode=st.mode,
|
|
owner_specialist=st.owner_specialist,
|
|
plan_id=st.plan_id,
|
|
plan_path=st.plan_path,
|
|
plan_content=st.plan_content,
|
|
plan_confirmed=st.plan_confirmed,
|
|
entered_at_ms=st.entered_at_ms,
|
|
updated_at_ms=st.updated_at_ms,
|
|
last_user_text_norm=user_text_norm,
|
|
plan_loop_count=nxt_count,
|
|
),
|
|
)
|
|
|
|
def confirm(self, *, session_id: str) -> PlanAgentStateV2:
|
|
st = self.refresh_plan_content(session_id=session_id)
|
|
return self._state_store.save(
|
|
session_id=session_id,
|
|
state=PlanAgentStateV2(
|
|
mode=PLAN_MODE_NORMAL,
|
|
owner_specialist=st.owner_specialist,
|
|
plan_id=st.plan_id,
|
|
plan_path=st.plan_path,
|
|
plan_content=st.plan_content,
|
|
plan_confirmed=True,
|
|
entered_at_ms=st.entered_at_ms,
|
|
updated_at_ms=st.updated_at_ms,
|
|
last_user_text_norm="",
|
|
plan_loop_count=0,
|
|
),
|
|
)
|
|
|
|
def build_approved_execution_message(self, *, state: PlanAgentStateV2, lang: str) -> str:
|
|
is_en = str(lang or "").startswith("en")
|
|
plan_path = str(state.plan_path or "").strip() or "unknown"
|
|
plan_content = str(state.plan_content or "").strip()
|
|
if plan_content:
|
|
if is_en:
|
|
return (
|
|
"User has approved your plan. You can now start implementation.\n\n"
|
|
f"Plan file: {plan_path}\n\n"
|
|
f"## Approved Plan\n{plan_content}"
|
|
)
|
|
return (
|
|
"用户已确认计划,你可以开始执行实现。\n\n"
|
|
f"计划文件:{plan_path}\n\n"
|
|
f"## 已确认计划\n{plan_content}"
|
|
)
|
|
return (
|
|
f"Plan approved. You can now start implementation. Plan file: {plan_path}"
|
|
if is_en
|
|
else f"计划已确认,你可以开始执行实现。计划文件:{plan_path}"
|
|
)
|
|
|
|
def exit_without_confirm(self, *, session_id: str) -> PlanAgentStateV2:
|
|
st = self._state_store.load(session_id=session_id)
|
|
return self._state_store.save(
|
|
session_id=session_id,
|
|
state=PlanAgentStateV2(
|
|
mode=PLAN_MODE_NORMAL,
|
|
owner_specialist=st.owner_specialist,
|
|
plan_id=st.plan_id,
|
|
plan_path=st.plan_path,
|
|
plan_content=st.plan_content,
|
|
plan_confirmed=False,
|
|
entered_at_ms=st.entered_at_ms,
|
|
updated_at_ms=st.updated_at_ms,
|
|
last_user_text_norm="",
|
|
plan_loop_count=0,
|
|
),
|
|
)
|
|
|
|
|
|
__all__ = ["PlanModeManagerV2"]
|
|
|