Files
Pyra/src/pyra/plugins/registry.py
T
curo1305 3d3ce694b9 feat(daemon): add core asyncio daemon, supervisor, and registry factories
- core.py: asyncio event loop entry point, PluginSupervisor with per-task
  restart (up to 10 times, 5s back-off), IPC dispatch, signal handling
  (SIGTERM/SIGHUP on POSIX), RotatingFileHandler, start_background() helper
- daemon/__init__.py: export public API
- plugins/registry.py: add get_daemon_task_factories() so supervisor can
  restart crashed tasks by re-calling plugin.daemon_tasks()[i]
- config/schema.py: add DaemonConfig.ipc_port for Windows TCP loopback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:33:57 +02:00

125 lines
4.4 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Callable, Coroutine
from pyra.plugins.base import AgentSpec, PyraPlugin, Tool
from pyra.plugins.loader import _log_error, load_plugins
from pyra.vault.reader import get_key
class PluginRegistry:
_instance: PluginRegistry | None = None
def __init__(self) -> None:
self._plugins: dict[str, PyraPlugin] = {}
self._tools: dict[str, Tool] = {}
@classmethod
def instance(cls) -> PluginRegistry:
if cls._instance is None:
cls._instance = cls()
return cls._instance
@classmethod
def reset(cls) -> None:
"""Reset singleton — for tests only."""
cls._instance = None
def load_all(self, plugins_dir: Path, enabled_names: list[str]) -> None:
all_plugins = load_plugins(plugins_dir)
self._plugins = {}
self._tools = {}
for plugin in all_plugins:
if plugin.name in enabled_names:
try:
plugin.on_load(get_key)
self._plugins[plugin.name] = plugin
for tool in plugin.tools():
self._tools[tool.name] = tool
except Exception as exc:
_log_error(plugin.name, exc)
def get_active_plugins(self) -> list[PyraPlugin]:
return list(self._plugins.values())
def get_all_tools(self) -> list[Tool]:
return list(self._tools.values())
def get_slash_commands(self) -> dict[str, Callable[[], None]]:
cmds: dict[str, Callable[[], None]] = {}
for plugin in self._plugins.values():
try:
cmds.update(plugin.slash_commands())
except Exception:
pass
return cmds
def get_system_prompt_additions(self) -> str:
parts: list[str] = []
for plugin in self._plugins.values():
try:
addition = plugin.system_prompt_addition()
if addition:
parts.append(addition.strip())
except Exception:
pass
return "\n\n".join(parts)
def get_daemon_tasks(self) -> list[Coroutine]: # type: ignore[type-arg]
tasks: list[Coroutine] = [] # type: ignore[type-arg]
for plugin in self._plugins.values():
try:
tasks.extend(plugin.daemon_tasks())
except Exception:
pass
return tasks
def get_daemon_task_factories(
self,
) -> list[tuple[str, Callable[[], Coroutine]]]: # type: ignore[type-arg]
"""Return (name, factory) pairs for all plugin daemon tasks.
Each factory re-calls plugin.daemon_tasks() to produce a fresh coroutine,
enabling the supervisor to restart crashed tasks without changing the plugin
protocol.
"""
factories: list[tuple[str, Callable[[], Coroutine]]] = [] # type: ignore[type-arg]
for plugin in self._plugins.values():
try:
n_tasks = len(plugin.daemon_tasks())
except Exception:
continue
for i in range(n_tasks):
name = f"{plugin.name}.task_{i}"
# Capture plugin and index by value so each closure is independent.
def _factory(p=plugin, idx=i) -> Coroutine: # type: ignore[type-arg]
return p.daemon_tasks()[idx]
factories.append((name, _factory))
return factories
def find_tool(self, name: str) -> Tool | None:
return self._tools.get(name)
def register_builtin(self, tool: Tool) -> None:
"""Register a built-in tool independent of plugins. Call after load_all."""
self._tools[tool.name] = tool
def get_agent(self, name: str) -> tuple[AgentSpec, list[Tool]] | None:
"""Return (AgentSpec, tools) for a named plugin agent, or None."""
plugin = self._plugins.get(name)
if plugin is None:
return None
spec = plugin.agent_spec()
if spec is None:
return None
return (spec, plugin.tools())
def list_agents(self) -> list[tuple[str, AgentSpec]]:
"""Return (plugin_name, AgentSpec) for all plugins that have agents."""
return [
(name, plugin.agent_spec())
for name, plugin in self._plugins.items()
if plugin.agent_spec() is not None
]