Coverage for merco/agents/subagent.py: 100%

56 statements  

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

1"""SubAgentManager — 子代理派发""" 

2from __future__ import annotations 

3 

4import logging 

5from typing import TYPE_CHECKING 

6 

7if TYPE_CHECKING: 

8 from merco.core.agent import Agent 

9 from merco.agents.profile import AgentProfileRegistry 

10 

11logger = logging.getLogger("merco.agents.subagent") 

12 

13 

14class SubAgentManager: 

15 """子代理派发管理器""" 

16 

17 def __init__(self, parent: "Agent", profile_registry: "AgentProfileRegistry" = None): 

18 self._parent = parent 

19 self._profiles = profile_registry 

20 self._active: dict[str, "Agent"] = {} 

21 

22 async def dispatch(self, todo_id: str, prompt: str, agent_name: str = "default") -> str: 

23 """派发子代理执行任务,返回 subagent_id""" 

24 # 1. 创建子 Agent 

25 sub_agent = await self._create_sub_agent(agent_name) 

26 

27 # 2. 更新 Todo 状态 

28 self._parent.todo_manager.update(todo_id, status="in_progress", assigned_to=sub_agent.session.id) 

29 

30 # 3. 执行子代理 

31 try: 

32 result = await sub_agent.run(prompt) 

33 # 4. 更新 Todo 结果 

34 self._parent.todo_manager.update(todo_id, status="completed", result=result) 

35 except Exception as e: 

36 logger.warning("子代理执行失败: %s", e) 

37 self._parent.todo_manager.update(todo_id, status="failed", result=str(e)) 

38 result = f"Error: {e}" 

39 

40 # 5. 结果注入父 context 

41 self._inject_result_to_parent(todo_id, result) 

42 

43 # 6. 触发事件 

44 await self._parent.hooks.emit("subagent.completed", todo_id=todo_id, result=result) 

45 

46 return sub_agent.session.id 

47 

48 async def _create_sub_agent(self, agent_name: str) -> "Agent": 

49 """创建子代理,根据 profile 配置 prompt/tools/model/limits""" 

50 from merco.core.agent import Agent 

51 from merco.core.session import Session 

52 from merco.agents.profile import ProfilePromptChunk 

53 from merco.tools.registry import ToolRegistry 

54 

55 # 查找 profile 

56 profile = None 

57 if self._profiles: 

58 profile = self._profiles.get(agent_name) or self._profiles.get("default") 

59 

60 config = self._parent.config 

61 tool_registry = self._parent.tool_registry 

62 

63 if profile: 

64 # model override 

65 if profile.model: 

66 import copy 

67 config = copy.deepcopy(config) 

68 config.model.provider = profile.model.get("provider", config.model.provider) 

69 config.model.model = profile.model.get("model", config.model.model) 

70 

71 # tools allowlist 

72 if profile.tools: 

73 tool_registry = ToolRegistry() 

74 for name in profile.tools: 

75 tool = self._parent.tool_registry.get(name) 

76 if tool: 

77 tool_registry.register(tool) 

78 

79 sub_agent = await Agent.create(config=config, tool_registry=tool_registry) 

80 # 强制新 session(Agent.create 会 resume_or_create 恢复父会话) 

81 sub_agent.session = Session(store=sub_agent._session_store) 

82 sub_agent._session_store.create_session(sub_agent.session.id) 

83 

84 if profile: 

85 sub_agent.prompt_builder.use(ProfilePromptChunk(profile)) 

86 if profile.limits.get("max_tool_calls"): 

87 sub_agent.config.max_tool_calls = profile.limits["max_tool_calls"] 

88 sub_agent._max_tool_calls = profile.limits["max_tool_calls"] 

89 

90 self._active[sub_agent.session.id] = sub_agent 

91 return sub_agent 

92 

93 def _inject_result_to_parent(self, todo_id: str, result: str): 

94 """把子代理结果注入父代理的 context""" 

95 self._parent.context.add({ 

96 "role": "tool", 

97 "content": f"[Todo {todo_id}] 子代理结果:\n{result}", 

98 "tool_call_id": f"todo_{todo_id}", 

99 })