OpenSpace/openspace/services/runtime_support/plan_mode.py
2026-07-17 11:43:42 +08:00

196 lines
6 KiB
Python

from __future__ import annotations
import hashlib
import re
from pathlib import Path
from typing import Any
from openspace.services.runtime_support.settings import get_openspace_config_home_dir
ENTER_PLAN_MODE_TOOL_NAME = "EnterPlanMode"
EXIT_PLAN_MODE_TOOL_NAME = "ExitPlanMode"
PLAN_MODE_ATTACHMENT_TURNS_BETWEEN_ATTACHMENTS = 5
PLAN_MODE_FULL_REMINDER_EVERY_N_ATTACHMENTS = 5
_PLAN_SLUGS_BY_SESSION: dict[str, str] = {}
_ACTIVE_PLAN_PREFIXES: set[str] = set()
def _session_key(session_id: str | None) -> str:
return str(session_id or "default")
def _sanitize_slug(value: str) -> str:
sanitized = re.sub(r"[^a-zA-Z0-9_-]+", "-", value).strip("-").lower()
return sanitized or "plan"
def get_plans_directory(config_home: str | Path | None = None) -> Path:
path = get_openspace_config_home_dir(config_home) / "plans"
path.mkdir(parents=True, exist_ok=True)
return path
def get_plan_slug(session_id: str | None = None) -> str:
key = _session_key(session_id)
cached = _PLAN_SLUGS_BY_SESSION.get(key)
if cached:
return cached
digest = hashlib.sha256(key.encode("utf-8")).hexdigest()[:10]
prefix = _sanitize_slug(key)[:40]
slug = f"{prefix}-{digest}" if prefix != digest else digest
_PLAN_SLUGS_BY_SESSION[key] = slug
return slug
def set_plan_slug(session_id: str | None, slug: str) -> None:
_PLAN_SLUGS_BY_SESSION[_session_key(session_id)] = _sanitize_slug(slug)
def clear_plan_slug(session_id: str | None = None) -> None:
_PLAN_SLUGS_BY_SESSION.pop(_session_key(session_id), None)
def get_plan_file_path(
session_id: str | None = None,
agent_id: str | None = None,
*,
config_home: str | Path | None = None,
) -> Path:
slug = get_plan_slug(session_id)
suffix = ""
if agent_id and agent_id != "primary":
suffix = f"-agent-{_sanitize_slug(str(agent_id))}"
path = get_plans_directory(config_home) / f"{slug}{suffix}.md"
register_active_plan_path(path, session_id=session_id)
return path
def get_plan(
session_id: str | None = None,
agent_id: str | None = None,
*,
config_home: str | Path | None = None,
) -> str | None:
path = get_plan_file_path(session_id, agent_id, config_home=config_home)
try:
return path.read_text(encoding="utf-8")
except OSError:
return None
def write_plan(
content: str,
session_id: str | None = None,
agent_id: str | None = None,
*,
config_home: str | Path | None = None,
) -> Path:
path = get_plan_file_path(session_id, agent_id, config_home=config_home)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
register_active_plan_path(path, session_id=session_id)
return path
def register_active_plan_path(path: str | Path, *, session_id: str | None = None) -> None:
try:
resolved = Path(path).expanduser().resolve()
except OSError:
resolved = Path(path).expanduser().absolute()
_ACTIVE_PLAN_PREFIXES.add(str(resolved))
if session_id:
try:
base = get_plans_directory() / get_plan_slug(session_id)
_ACTIVE_PLAN_PREFIXES.add(str(base.expanduser().resolve()))
except OSError:
pass
def is_active_plan_file(path: str | Path) -> bool:
try:
resolved = str(Path(path).expanduser().resolve())
except OSError:
resolved = str(Path(path).expanduser().absolute())
if resolved in _ACTIVE_PLAN_PREFIXES:
return True
for prefix in tuple(_ACTIVE_PLAN_PREFIXES):
if resolved.startswith(prefix) and resolved.endswith(".md"):
return True
return False
def register_plan_mode_permission_predicates() -> None:
try:
from openspace.grounding.core.permissions.filesystem import (
register_internal_path_predicate,
)
except Exception:
return
marker = "_openspace_plan_mode_predicates_registered"
if getattr(register_plan_mode_permission_predicates, marker, False):
return
register_internal_path_predicate(
category="editable",
reason="Plan files for current session are allowed for writing",
predicate=is_active_plan_file,
)
register_internal_path_predicate(
category="readable",
reason="Plan files for current session are allowed for reading",
predicate=is_active_plan_file,
)
setattr(register_plan_mode_permission_predicates, marker, True)
def enter_plan_mode(context: Any) -> dict[str, Any]:
previous = str(getattr(context, "permission_mode", None) or "default")
if previous == "plan":
previous = str(getattr(context, "pre_plan_mode", None) or "default")
setattr(context, "pre_plan_mode", previous)
setattr(context, "permission_mode", "plan")
perm_ctx = getattr(context, "permission_context", None)
if perm_ctx is not None:
from dataclasses import replace
context.permission_context = replace(
perm_ctx,
mode="plan",
pre_plan_mode=previous,
)
session_id = getattr(context, "session_id", None)
agent_id = getattr(context, "agent_id", None)
plan_path = get_plan_file_path(session_id, agent_id)
setattr(context, "plan_file_path", str(plan_path))
setattr(context, "plan_mode_exited_in_session", False)
return {
"previous_mode": previous,
"plan_file_path": str(plan_path),
}
def exit_plan_mode(context: Any) -> dict[str, Any]:
previous = str(getattr(context, "pre_plan_mode", None) or "default")
if previous == "plan":
previous = "default"
setattr(context, "permission_mode", previous)
setattr(context, "pre_plan_mode", None)
setattr(context, "plan_mode_exit_pending", True)
setattr(context, "plan_mode_exited_in_session", True)
perm_ctx = getattr(context, "permission_context", None)
if perm_ctx is not None:
from dataclasses import replace
context.permission_context = replace(
perm_ctx,
mode=previous,
pre_plan_mode=None,
)
return {"restored_mode": previous}
register_plan_mode_permission_predicates()