Files
agent-desktop/plugins/bundle/qwenpaw-pet/plugin.py
T

172 lines
5.5 KiB
Python

# -*- coding: utf-8 -*-
"""QwenPaw Pet backend plugin entry point."""
# pylint: disable=wrong-import-position,wrong-import-order
import atexit
import logging
import os
import sys
from pathlib import Path
# ``qwenpaw plugin install`` execs this file as a plain module (no
# package), so sibling modules are not reachable via relative imports
# unless the plugin directory is on sys.path before importing them.
_plugin_dir = str(Path(__file__).resolve().parent)
if _plugin_dir not in sys.path:
sys.path.insert(0, _plugin_dir)
# Evict stale cached sibling modules so hot-reload after plugin
# reinstall picks up the new files instead of the previous version
# lingering in ``sys.modules``.
for _sib in (
"emitter",
"patch_runner",
"patch_approval",
"router",
"pet_paths",
):
_cached = sys.modules.get(_sib)
if _cached is None:
continue
_cached_file = getattr(_cached, "__file__", None) or ""
if _cached_file and os.path.realpath(_cached_file).startswith(
os.path.realpath(_plugin_dir) + os.sep,
):
del sys.modules[_sib]
from pineagents.plugins.api import PluginApi # noqa: E402
from emitter import ( # noqa: E402
emit_pet_event,
ensure_desktop_available,
stop_desktop,
)
from patch_approval import ( # noqa: E402
patch_approval_service,
restore_approval_service,
)
from patch_runner import ( # noqa: E402
patch_agent_runner,
restore_agent_runner,
)
from router import build_router # noqa: E402
# Logger uses ``qwenpaw.*`` so messages appear in the project logger
# (``~/.qwenpaw/qwenpaw.log``).
logger = logging.getLogger("qwenpaw.pet_desktop")
def _atexit_stop_pet_desktop() -> None:
"""Best-effort stop when the interpreter exits without lifespan hooks."""
try:
stop_desktop(force=True, aggressive=True, grace=5.0)
except Exception:
logger.debug(
"QwenPaw Pet: atexit stop skipped or failed",
exc_info=True,
)
class QwenPawPetPlugin:
"""Emit QwenPaw backend lifecycle events to the desktop pet."""
def register(self, api: PluginApi):
"""Register startup/shutdown hooks and plugin HTTP routes."""
logger.info("Registering QwenPaw Pet plugin")
# Runtime patches (``AgentRunner`` / ``ApprovalService``) are
# applied exclusively from the startup hook below — applying them
# here at import time runs before QwenPaw has finished wiring up
# the affected classes and silently swallowing the import error
# would leave the plugin in a broken state.
api.register_startup_hook(
hook_name="qwenpaw_pet_startup",
callback=self._startup,
priority=80,
)
api.register_shutdown_hook(
hook_name="qwenpaw_pet_shutdown",
callback=self._shutdown,
priority=120,
)
api.register_http_router(
build_router(),
prefix="/qwenpaw-pet",
tags=["qwenpaw-pet"],
)
atexit.register(_atexit_stop_pet_desktop)
logger.info("QwenPaw Pet plugin registered")
def _startup(self):
"""Patch the runner and notify the desktop.
Patch failures (e.g. an upstream rename of ``AgentRunner`` /
``ApprovalService``) surface as ``logger.exception`` so the
plugin install system can flag them; we still attempt to keep
the desktop autostart and ``qwenpaw.startup`` emit going so the
UI is never silently dead.
"""
try:
patch_agent_runner()
except Exception:
logger.exception(
"QwenPaw Pet: failed to patch AgentRunner; "
"lifecycle events will be unavailable",
)
try:
patch_approval_service()
except Exception:
logger.exception(
"QwenPaw Pet: failed to patch ApprovalService; "
"approval events will be unavailable",
)
try:
ensure_desktop_available()
emit_pet_event(
"qwenpaw.startup",
text="QwenPaw started",
duration_ms=1500,
)
logger.info("QwenPaw Pet startup hook complete")
except Exception:
logger.exception("QwenPaw Pet startup hook failed")
def _shutdown(self):
"""Notify the desktop, terminate it, and restore the runner patch.
The pet desktop is treated as a child of QwenPaw: when QwenPaw
exits, the floating window goes with it. ``stop_desktop`` only
kills a process that this plugin instance spawned (so a desktop
started independently via ``python -m qwenpaw_pet_desktop start``
is left alone), and the whole behaviour can be opted out of via
``QWENPAW_PET_STOP_ON_SHUTDOWN=0``.
"""
try:
emit_pet_event("qwenpaw.shutdown", text="", duration_ms=500)
except Exception:
logger.warning(
"QwenPaw Pet: shutdown event emit failed",
exc_info=True,
)
try:
result = stop_desktop(force=True, aggressive=True, grace=5.0)
logger.info("QwenPaw Pet: stop_desktop result=%s", result)
except Exception:
logger.exception("QwenPaw Pet: failed to stop desktop process")
try:
restore_approval_service()
restore_agent_runner()
except Exception:
logger.exception("QwenPaw Pet: failed to restore class methods")
logger.info("QwenPaw Pet shutdown hook complete")
plugin = QwenPawPetPlugin()