Coverage for merco/plugins/base.py: 96%

47 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-07 14:04 +0800

1"""Plugin base class + PluginContext""" 

2from __future__ import annotations 

3 

4from abc import ABC, abstractmethod 

5from typing import TYPE_CHECKING 

6 

7if TYPE_CHECKING: 

8 from merco.hooks.registry import HookRegistry 

9 from merco.tools.registry import ToolRegistry 

10 from merco.core.agent import PromptBuilder, PromptChunk, Agent 

11 from merco.core.pipeline import RecoveryPipeline, ResultPipeline 

12 from merco.memory.save_pipeline import MemorySavePipeline 

13 from merco.memory.recall import HybridRecaller, BaseRecaller 

14 from merco.core.config import MercoConfig 

15 from merco.observability.observer import Observer 

16 from merco.tools.base import BaseTool 

17 from merco.todo.manager import TodoManager 

18 from merco.agents.profile import AgentProfileRegistry 

19 from merco.agents.subagent import SubAgentManager 

20 from merco.context.pipeline import ContextPipeline 

21 from merco.memory.backend import MemoryBackendRegistry 

22 from merco.core.loop_policy import LoopPolicyRegistry 

23 from merco.skills.registry import SkillRegistry 

24 from merco.mcp.manager import MCPServerManager 

25 from merco.scheduler.cron import CronScheduler 

26 from typing import Callable 

27 

28 

29_PIPELINE_WHITELIST = { 

30 "result_pipeline", 

31 "recovery_pipeline", 

32 "memory_save_pipeline", 

33 "context_pipeline", 

34} 

35 

36 

37class Plugin(ABC): 

38 """merco plugin base class""" 

39 name: str = "" # unique identifier 

40 version: str = "" # semantic version 

41 description: str = "" # one-line description 

42 

43 @abstractmethod 

44 async def activate(self, ctx: "PluginContext") -> None: 

45 """Called on activation; ctx provides all extension points""" 

46 ... 

47 

48 async def deactivate(self) -> None: 

49 """Called on deactivation to clean up resources. Default no-op.""" 

50 pass 

51 

52 

53class PluginContext: 

54 """Extension point entry for plugins, injected during activate""" 

55 

56 def __init__( 

57 self, 

58 hooks: "HookRegistry", 

59 tool_registry: "ToolRegistry", 

60 prompt_builder: "PromptBuilder", 

61 recovery_pipeline: "RecoveryPipeline", 

62 result_pipeline: "ResultPipeline", 

63 memory_save_pipeline: "MemorySavePipeline", 

64 recaller: "HybridRecaller", 

65 config: "MercoConfig", 

66 observer: "Observer" = None, 

67 todo_manager: "TodoManager" = None, 

68 sub_agent_manager: "SubAgentManager" = None, 

69 context_pipeline: "ContextPipeline" = None, 

70 agent_profiles: "AgentProfileRegistry" = None, 

71 memory_backends: "MemoryBackendRegistry" = None, 

72 loop_policies: "LoopPolicyRegistry" = None, 

73 agent: "Agent" = None, 

74 skill_registry: "SkillRegistry" = None, 

75 mcp_manager: "MCPServerManager" = None, 

76 scheduler: "CronScheduler" = None, 

77 ): 

78 self.hooks = hooks 

79 self.tool_registry = tool_registry 

80 self.prompt_builder = prompt_builder 

81 self.recovery_pipeline = recovery_pipeline 

82 self.result_pipeline = result_pipeline 

83 self.memory_save_pipeline = memory_save_pipeline 

84 self.recaller = recaller 

85 self.config = config 

86 self.observer = observer 

87 self.todo_manager = todo_manager 

88 self.sub_agent_manager = sub_agent_manager 

89 self.context_pipeline = context_pipeline 

90 self.agent_profiles = agent_profiles 

91 self.memory_backends = memory_backends 

92 self.loop_policies = loop_policies 

93 self.agent = agent 

94 self.skill_registry = skill_registry 

95 self.mcp_manager = mcp_manager 

96 self.scheduler = scheduler 

97 

98 def on(self, event: str, handler: "Callable") -> None: 

99 """Subscribe to event (convenience method)""" 

100 self.hooks.on(event, handler) 

101 

102 def register_tool(self, tool: "BaseTool") -> None: 

103 """Register a tool""" 

104 self.tool_registry.register(tool) 

105 

106 def add_prompt_chunk(self, chunk: "PromptChunk") -> None: 

107 """Inject a system prompt chunk""" 

108 self.prompt_builder.use(chunk) 

109 

110 def add_processor(self, pipeline_name: str, processor) -> None: 

111 """加处理器到白名单管线""" 

112 if pipeline_name not in _PIPELINE_WHITELIST: 

113 raise ValueError(f"Pipeline '{pipeline_name}' not extensible") 

114 pipeline = getattr(self, pipeline_name, None) 

115 if pipeline and hasattr(pipeline, 'use'): 

116 pipeline.use(processor) 

117 

118 def add_recaller(self, recaller: "BaseRecaller") -> None: 

119 """Add a memory recaller""" 

120 self.recaller.add(recaller)