"""Centralized runtime loading for Beaver agents.""" from __future__ import annotations 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.memory.curated.store import MemoryStore from beaver.services.memory_service import MemoryService from beaver.skills import SkillAssembler, SkillsLoader from beaver.tools import ObjectBackedTool, ToolExecutor, ToolRegistry from beaver.tools.builtins import EchoTool, MemoryTool, SessionSearchTool, SkillViewTool @dataclass(slots=True) class EngineLoadResult: """描述当前 agent runtime 已经装好的依赖。 这里同时保留两类字段: 1. `tools/skills/memory_stores/permissions` - 便于做状态展示、调试、轻量测试 2. `session_manager/tool_registry/...` - 供真正的运行时主链直接使用 """ workspace: Path 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_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, session_manager: SessionManager | None = None, curated_memory_store: MemoryStore | None = None, memory_service: MemoryService | None = None, tool_registry: ToolRegistry | None = None, context_builder: ContextBuilder | None = None, skills_loader: SkillsLoader | None = None, skill_assembler: SkillAssembler | None = None, ) -> None: self.workspace = Path(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._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)), ] ) context_builder = self._context_builder or ContextBuilder() tool_executor = ToolExecutor(tool_registry) skill_assembler = self._skill_assembler or SkillAssembler(skills_loader) result = EngineLoadResult( workspace=workspace, 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_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