Files
LZH-YS1998 08e48c2f9c fix(logging): replace stdlib-style exc_info kwargs with loguru opt(exception=)
Loguru has no exc_info kwarg: extra kwargs are str.format() arguments, so
logger.error(f"...{e}", exc_info=True) forces .format() on the rendered
message — any error text containing braces (e.g. a JSON error body) raises
KeyError FROM the log call itself, escaping the surrounding except block and
killing the caller (observed: whole agent turns dying in benchmark runs).
The intended traceback was also never logged, since exc_info is not a loguru
feature.

Batch fix of all 143 sites across 11 files:
  logger.X(msg, exc_info=True) -> logger.opt(exception=True).X(msg)
  (one exc_info=exc site -> opt(exception=exc))
Messages are byte-identical; with the kwarg gone loguru never calls
.format(), so brace-containing f-string messages are inert.

Verified: AST post-conditions per file, py_compile, import smoke of all
modules, behavioral equivalence of the 3 patterns, full unit suite (1549
passed) with a failure set identical to the pristine tree (22 pre-existing,
zero regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Also add pixel-agents to the README acknowledgements.
2026-07-03 20:07:37 +08:00

202 lines
8.3 KiB
Python

"""Shared Office service context.
This module intentionally owns runtime wiring that was previously duplicated or
buried inside the WebSocket handler: project validation, project-engine
delegation, active mode defaults, and access to UI persistence stores.
"""
from __future__ import annotations
import asyncio
import inspect
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Awaitable, Callable, Optional
from loguru import logger
from opc.core.config import get_project_workplace
LoadOrgConfigHook = Callable[[Optional[str]], bool]
SetActiveOrgHook = Callable[[str], Awaitable[None]]
GetActiveOrgHook = Callable[[], Awaitable[str]]
PersistRuntimeConfigHook = Callable[[], None]
RebindEngineConfigHook = Callable[[Any], None]
AsyncNoArgHook = Callable[[], Awaitable[Any]]
CancelSessionTasksHook = Callable[[str], None]
CancelTaskTreeHook = Callable[..., Awaitable[list[str]]]
RuntimeControlHook = Callable[..., Awaitable[Any]]
@dataclass
class ModeState:
exec_mode: str = "task"
company_profile: str = "corporate"
task_preferred_agent: str = "native"
class OfficeServiceContext:
"""Dependency holder shared by Office UI, CLI, and CLI board services."""
def __init__(
self,
*,
engine: Any,
agent_store: Any,
chat_store: Any,
event_adapter: Any,
mode_state: ModeState | None = None,
) -> None:
self.root_engine = engine
self.active_engine = engine
self.agent_store = agent_store
self.chat_store = chat_store
self.event_adapter = event_adapter
self.mode_state = mode_state or ModeState()
self.active_project_id = self.normalize_project_id(getattr(engine, "project_id", None))
self.project_switch_lock = asyncio.Lock()
self.config_lock = asyncio.Lock()
self.background_tasks: set[asyncio.Task[Any]] = set()
self.task_bg_map: dict[str, set[asyncio.Task[Any]]] = {}
self.task_bg_context: dict[asyncio.Task[Any], dict[str, Any]] = {}
self.session_to_task: dict[str, str] = {}
self.active_runtime_children: dict[str, str] = {}
self.stop_requested_task_ids: set[str] = set()
self.task_locks: dict[str, asyncio.Lock] = {}
self.task_lock_holders: dict[str, asyncio.Task[Any]] = {}
self.load_active_org_config: LoadOrgConfigHook | None = None
self.set_active_saved_org_name: SetActiveOrgHook | None = None
self.get_active_saved_org_name: GetActiveOrgHook | None = None
self.on_engine_activated: Callable[[Any, str], None] | None = None
self.persist_runtime_config: PersistRuntimeConfigHook | None = None
self.rebind_engine_config: RebindEngineConfigHook | None = None
self.sync_role_map: AsyncNoArgHook | None = None
self.ensure_custom_role_agents: AsyncNoArgHook | None = None
self.broadcast_snapshot: AsyncNoArgHook | None = None
self.cancel_session_tasks: CancelSessionTasksHook | None = None
self.cancel_task_tree: CancelTaskTreeHook | None = None
self.runtime_stop_hook: RuntimeControlHook | None = None
self.runtime_continue_hook: RuntimeControlHook | None = None
@property
def engine(self) -> Any:
return self.active_engine
@property
def opc_home(self) -> Path:
return Path(getattr(self.root_engine, "opc_home", Path.cwd() / ".opc"))
@staticmethod
def normalize_project_id(project_id: Any) -> str:
return str(project_id or "default").strip() or "default"
@staticmethod
def is_safe_project_id(project_id: str) -> bool:
return bool(re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", project_id or ""))
@staticmethod
def store_is_ready(store: Any) -> bool:
if store is None:
return False
ready = getattr(store, "is_ready", True)
return bool(ready)
def active_engine_project_id(self) -> str:
return self.normalize_project_id(getattr(self.active_engine, "project_id", None) or self.active_project_id)
def rebind_config(self, config: Any) -> None:
if self.rebind_engine_config is not None:
self.rebind_engine_config(config)
return
self.engine.config = config
org_engine = getattr(self.engine, "org_engine", None)
if org_engine is not None:
org_engine.config = config
talent_market = getattr(self.engine, "talent_market", None)
if talent_market is not None:
talent_market.config = config
if hasattr(self.engine, "_runtime_config_signature"):
self.engine._runtime_config_signature = None
def is_custom_org_editable(self) -> bool:
mode = str(getattr(self.mode_state, "exec_mode", "") or "").strip().lower()
profile = str(getattr(self.mode_state, "company_profile", "") or "").strip().lower()
cfg_org = getattr(getattr(self.engine, "config", None), "org", None)
cfg_profile = str(getattr(cfg_org, "company_profile", "") or "").strip().lower()
org_id = str(getattr(cfg_org, "organization_id", "") or "").strip().lower()
return (
mode in {"org", "custom"}
and profile == "custom"
and cfg_profile == "custom"
and org_id != "corporate"
)
def project_dir(self, project_id: str) -> Path:
return self.opc_home / "projects" / self.normalize_project_id(project_id)
def project_workplace(self, project_id: str) -> Path:
hook = getattr(self, "project_workplace_hook", None)
if callable(hook):
return Path(hook(self.normalize_project_id(project_id)))
return get_project_workplace(self.normalize_project_id(project_id))
def list_project_entries(self) -> list[dict[str, str]]:
projects_dir = self.opc_home / "projects"
projects: list[dict[str, str]] = []
if projects_dir.is_dir():
for entry in sorted(projects_dir.iterdir()):
if entry.is_dir():
projects.append({"id": entry.name, "name": entry.name})
if not any(project["id"] == "default" for project in projects):
projects.insert(0, {"id": "default", "name": "default"})
return projects
def project_exists(self, project_id: str) -> bool:
normalized = self.normalize_project_id(project_id)
if normalized == "default":
return True
return self.project_dir(normalized).is_dir()
async def engine_for_project(self, project_id: str) -> Any:
normalized = self.normalize_project_id(project_id)
root = self.root_engine
current_root_project = self.normalize_project_id(getattr(root, "project_id", None))
if normalized == current_root_project:
engine = root
else:
delegate_getter = getattr(root, "_get_project_delegate", None)
if not callable(delegate_getter):
raise RuntimeError("Project switching requires OPCEngine project delegates.")
maybe_engine = delegate_getter(normalized)
engine = await maybe_engine if inspect.isawaitable(maybe_engine) else maybe_engine
wire = getattr(self, "wire_engine_callbacks", None)
if callable(wire):
try:
wire(engine)
except Exception:
logger.opt(exception=True).debug("Failed to wire service project engine callbacks")
return engine
async def activate_project(self, project_id: str) -> Any:
engine = await self.engine_for_project(project_id)
self.active_engine = engine
self.active_project_id = self.normalize_project_id(getattr(engine, "project_id", None) or project_id)
if self.on_engine_activated is not None:
self.on_engine_activated(engine, self.active_project_id)
else:
ensure_attachment_store = getattr(engine, "_ensure_attachment_store", None)
if callable(ensure_attachment_store):
ensure_attachment_store()
return engine
def get_task_lock(self, task_id: str) -> asyncio.Lock:
prev_holder = self.task_lock_holders.get(task_id)
if prev_holder is not None and prev_holder.done():
self.task_locks.pop(task_id, None)
self.task_lock_holders.pop(task_id, None)
lock = self.task_locks.get(task_id)
if lock is None:
lock = asyncio.Lock()
self.task_locks[task_id] = lock
return lock