cc24257ab0
The initial daemon_tasks() call to count tasks created coroutines that were immediately discarded, triggering RuntimeWarning "coroutine never awaited". Explicitly close them after counting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.5 KiB
Python
128 lines
4.5 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:
|
|
initial = plugin.daemon_tasks()
|
|
n_tasks = len(initial)
|
|
for c in initial:
|
|
c.close() # prevent "coroutine never awaited" RuntimeWarning
|
|
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
|
|
]
|