feat(runtime): sync declarative plugins at boot

This commit is contained in:
2026-06-16 11:58:01 +08:00
parent a34b1219bc
commit 54bced4251
2 changed files with 180 additions and 9 deletions

View File

@ -12,10 +12,14 @@ from beaver.coordinator.registry import AgentRegistry
from beaver.engine.context import ContextBuilder
from beaver.engine.session import SessionManager
from beaver.foundation.config import BeaverConfig, load_config
from beaver.foundation.utils.file_lock import WorkspaceWriteLock, WorkspaceWriteLockBusy
from beaver.integrations.mcp import MCPConnectionManager
from beaver.memory.curated.store import MemoryStore
from beaver.memory.runs import RunMemoryStore
from beaver.memory.skills import SkillLearningStore
from beaver.plugins.discovery import discover_plugins
from beaver.plugins.skills import PluginManager
from beaver.plugins.state import PluginStateStore
from beaver.services.memory_service import MemoryService
from beaver.skills.drafts import DraftService
from beaver.skills.learning import EvidenceSelector, SkillDraftSynthesizer, SkillLearningPipelineService, SkillLearningService
@ -94,6 +98,8 @@ class EngineLoadResult:
skill_publisher: SkillPublisher | None = None
skill_learning_service: SkillLearningService | None = None
skill_learning_pipeline: SkillLearningPipelineService | None = None
plugin_manager: PluginManager | None = None
plugins: list[dict] = field(default_factory=list)
agent_registry: AgentRegistry | None = None
task_skill_resolver: TaskSkillResolver | None = None
task_service: TaskService | None = None
@ -168,6 +174,7 @@ class EngineLoader:
skill_publisher: SkillPublisher | None = None,
skill_learning_service: SkillLearningService | None = None,
skill_learning_pipeline: SkillLearningPipelineService | None = None,
plugin_manager: PluginManager | None = None,
agent_registry: AgentRegistry | None = None,
task_skill_resolver: TaskSkillResolver | None = None,
task_service: TaskService | None = None,
@ -193,6 +200,7 @@ class EngineLoader:
self._skill_publisher = skill_publisher
self._skill_learning_service = skill_learning_service
self._skill_learning_pipeline = skill_learning_pipeline
self._plugin_manager = plugin_manager
self._agent_registry = agent_registry
self._task_skill_resolver = task_skill_resolver
self._task_service = task_service
@ -209,7 +217,11 @@ class EngineLoader:
memory_service = self._memory_service or MemoryService(curated_root, store=curated_memory_store)
memory_service.initialize()
run_memory_store = self._run_memory_store or RunMemoryStore(workspace / "memory" / "runs")
skill_learning_store = self._skill_learning_store or SkillLearningStore(workspace / "memory" / "skills")
write_lock = WorkspaceWriteLock(workspace)
skill_learning_store = self._skill_learning_store or SkillLearningStore(
workspace / "memory" / "skills",
write_lock=write_lock,
)
tool_registry = self._tool_registry or ToolRegistry()
skill_spec_store = self._skill_spec_store or SkillSpecStore(workspace)
@ -264,21 +276,40 @@ class EngineLoader:
evidence_selector=evidence_selector,
synthesizer=SkillDraftSynthesizer(),
)
safety_checker = SkillDraftSafetyChecker(
allowed_tool_names={spec.name for spec in tool_registry.list_specs()},
allowed_tool_prefixes={
f"mcp_{server_id}_"
for server_id in self.config.tools.mcp_servers
if str(server_id).strip()
},
)
discovery = discover_plugins(workspace, search_paths=self.config.plugins.search_paths)
plugin_manager = self._plugin_manager or PluginManager(
workspace=workspace,
manifests=discovery.manifests,
discovery_errors=discovery.errors,
state_store=PluginStateStore(workspace),
skill_store=skill_spec_store,
learning_store=skill_learning_store,
publisher=skill_publisher,
safety_checker=safety_checker,
write_lock=write_lock,
)
if self.config.plugins.auto_sync:
try:
plugin_manager.sync_enabled(blocking=False)
except WorkspaceWriteLockBusy:
pass
skill_learning_pipeline = self._skill_learning_pipeline or SkillLearningPipelineService(
learning_store=skill_learning_store,
learning_service=skill_learning_service,
draft_service=draft_service,
review_service=review_service,
publisher=skill_publisher,
safety_checker=SkillDraftSafetyChecker(
allowed_tool_names={spec.name for spec in tool_registry.list_specs()},
allowed_tool_prefixes={
f"mcp_{server_id}_"
for server_id in self.config.tools.mcp_servers
if str(server_id).strip()
},
),
safety_checker=safety_checker,
evaluator=SkillDraftEvaluator(run_memory_store),
publish_observer=plugin_manager.on_skill_published,
)
agent_registry = self._agent_registry or AgentRegistry(workspace)
task_skill_resolver = self._task_skill_resolver or TaskSkillResolver(
@ -317,6 +348,8 @@ class EngineLoader:
skill_publisher=skill_publisher,
skill_learning_service=skill_learning_service,
skill_learning_pipeline=skill_learning_pipeline,
plugin_manager=plugin_manager,
plugins=_plugin_summaries(plugin_manager),
agent_registry=agent_registry,
task_skill_resolver=task_skill_resolver,
task_service=task_service,
@ -336,3 +369,35 @@ def _close_mcp_manager(manager: MCPConnectionManager) -> None:
asyncio.run(manager.close())
return
loop.create_task(manager.close())
def _plugin_summaries(manager: PluginManager) -> list[dict]:
summaries: list[dict] = []
for state in manager.list_plugins():
manifest = manager.manifests.get(state.plugin_id)
summaries.append(
{
"id": state.plugin_id,
"name": manifest.name if manifest is not None else state.plugin_id,
"discovered_version": manifest.version if manifest is not None else None,
"installed_version": state.installed_version,
"enabled": state.enabled,
"status": state.status,
"last_error": state.last_error,
"manifest_path": manifest.display_path if manifest is not None else state.manifest_path,
"updates_paused": state.updates_paused,
"skills": [
{
"name": name,
"status": binding.status,
"current_beaver_version": binding.current_beaver_version,
"accepted_upstream_tree_hash": binding.accepted_upstream_tree_hash,
"observed_upstream_tree_hash": binding.observed_upstream_tree_hash,
"accepted_beaver_version": binding.accepted_beaver_version,
"pending_candidate_id": binding.pending_candidate_id,
}
for name, binding in sorted(state.skills.items())
],
}
)
return summaries