"""Centralized runtime loading for Beaver agents.""" from __future__ import annotations import os from dataclasses import dataclass, field from pathlib import Path from typing import Callable from beaver.engine.context import ContextBuilder from beaver.engine.session import SessionManager from beaver.foundation.config import BeaverConfig, load_config from beaver.memory.curated.store import MemoryStore from beaver.services.memory_service import MemoryService from beaver.skills import SkillAssembler, SkillsLoader from beaver.tools import ObjectBackedTool, ToolAssembler, ToolExecutor, ToolRegistry from beaver.tools.builtins import ( EchoTool, ListDirectoryTool, MemoryTool, ReadFileTool, SearchFilesTool, SessionSearchTool, SkillViewTool, ) @dataclass(slots=True) class EngineLoadResult: """描述当前 agent runtime 已经装好的依赖。 这里同时保留两类字段: 1. `tools/skills/memory_stores/permissions` - 便于做状态展示、调试、轻量测试 2. `session_manager/tool_registry/...` - 供真正的运行时主链直接使用 """ workspace: Path config: BeaverConfig = field(default_factory=BeaverConfig) tools: list[str] = field(default_factory=list) skills: list[str] = field(default_factory=list) memory_stores: list[str] = field(default_factory=list) permissions: list[str] = field(default_factory=list) session_manager: SessionManager | None = None curated_memory_store: MemoryStore | None = None memory_service: MemoryService | None = None tool_registry: ToolRegistry | None = None tool_assembler: ToolAssembler | None = None tool_executor: ToolExecutor | None = None context_builder: ContextBuilder | None = None skills_loader: SkillsLoader | None = None skill_assembler: SkillAssembler | None = None closeables: list[tuple[str, Callable[[], None]]] = field(default_factory=list, repr=False) closed: bool = False def register_closeable(self, name: str, close_fn: Callable[[], None]) -> None: """登记一个由 runtime 统一关闭的资源。""" self.closeables.append((name, close_fn)) def close(self) -> None: """按后进先出顺序关闭 runtime 资源。 这一步先保持同步、最小、可组合: 1. 只管理已经明确需要关闭的资源 2. 暂不引入 async shutdown 协议 3. 为后续 Web/Gateway lifespan 留统一入口 """ if self.closed: return errors: list[tuple[str, BaseException]] = [] for name, close_fn in reversed(self.closeables): try: close_fn() except BaseException as exc: # pragma: no cover - defensive cleanup path errors.append((name, exc)) self.closed = True if errors: parts = ", ".join(f"{name}: {exc}" for name, exc in errors) raise RuntimeError(f"Runtime shutdown failed for {parts}") class EngineLoader: """为任意 Beaver agent 装载共享 runtime 能力。 当前先做“最小可运行主链”需要的装配: - session manager - curated memory store - context builder - built-in tools - tool executor 等主链跑稳后,再把 skills、权限、MCP、delegation 逐步加进来。 """ def __init__( self, *, workspace: str | Path | None = None, config_path: str | Path | None = None, config: BeaverConfig | None = None, session_manager: SessionManager | None = None, curated_memory_store: MemoryStore | None = None, memory_service: MemoryService | None = None, tool_registry: ToolRegistry | None = None, tool_assembler: ToolAssembler | None = None, context_builder: ContextBuilder | None = None, skills_loader: SkillsLoader | None = None, skill_assembler: SkillAssembler | None = None, ) -> None: self.config = config or load_config(workspace=workspace, config_path=config_path) configured_workspace = self.config.agents_defaults.workspace env_workspace = os.getenv("BEAVER_WORKSPACE") self.workspace = Path(workspace or configured_workspace or env_workspace or Path.cwd()) self._session_manager = session_manager self._curated_memory_store = curated_memory_store self._memory_service = memory_service self._tool_registry = tool_registry self._tool_assembler = tool_assembler self._context_builder = context_builder self._skills_loader = skills_loader self._skill_assembler = skill_assembler def load(self) -> EngineLoadResult: """装配当前主链需要的最小 runtime 对象。""" workspace = self.workspace session_manager = self._session_manager or SessionManager(workspace) curated_root = workspace / "memory" / "curated" curated_memory_store = self._curated_memory_store or MemoryStore(curated_root) memory_service = self._memory_service or MemoryService(curated_root, store=curated_memory_store) memory_service.initialize() tool_registry = self._tool_registry or ToolRegistry() skills_loader = self._skills_loader or SkillsLoader(workspace) if self._tool_registry is None: # 这里先注册最小工具集,满足主链的 tool loop。 tool_registry.register_many( [ ObjectBackedTool(EchoTool()), ObjectBackedTool(MemoryTool(store=memory_service.get_store())), ObjectBackedTool(SkillViewTool(loader=skills_loader)), ObjectBackedTool(SessionSearchTool(db=session_manager)), ObjectBackedTool(ListDirectoryTool()), ObjectBackedTool(ReadFileTool()), ObjectBackedTool(SearchFilesTool()), ] ) context_builder = self._context_builder or ContextBuilder() tool_assembler = self._tool_assembler or ToolAssembler() tool_executor = ToolExecutor(tool_registry) skill_assembler = self._skill_assembler or SkillAssembler(skills_loader) result = EngineLoadResult( workspace=workspace, config=self.config, tools=[spec.name for spec in tool_registry.list_specs()], skills=[record.name for record in skills_loader.list_skills(filter_unavailable=False)], memory_stores=["curated"], permissions=[], session_manager=session_manager, curated_memory_store=memory_service.get_store(), memory_service=memory_service, tool_registry=tool_registry, tool_assembler=tool_assembler, tool_executor=tool_executor, context_builder=context_builder, skills_loader=skills_loader, skill_assembler=skill_assembler, ) if self._session_manager is None: result.register_closeable("session_manager", session_manager.close) return result