添加 DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS 配置项以控制团队节点的最大工具迭代次数, 并修改 LocalAgentRunner 中的逻辑来使用此默认值当 envelope 中未指定时。 fix(runtime): 修复团队节点运行成功判断逻辑 更新运行成功判断条件,将 finish_reason 为 "max_tool_iterations_finalized" 的情况 视为运行失败,并添加对原始工具调用输出的检测,避免将其误判为成功完成。 feat(mcp): 添加团队工作流MCP工具类别支持 增加新的本地MCP工具类别 "team_workflow" 及其对应的工具创建功能, 为团队工作流提供本地工具支持。 refactor(engine): 调整AgentLoop最大工具迭代次数设置 将 AgentProfile 中的默认 max_tool_iterations 从 30 增加到 100, 同时移除 TaskExecutionPlanner 构造函数中的重复参数传递。 perf(mcp): 优化MCP连接管理避免重复连接 添加 mcp_connected 标志来跟踪MCP连接状态,确保 connect_all 只执行一次, 提高性能并避免不必要的重复连接。 refactor(skills): 移除技能团队模板相关功能 移除与技能团队模板相关的代码,包括解析、存储和处理逻辑, 简化技能记录结构和加载流程。 feat(process): 增强会话过程投影器功能 添加技能激活快照事件处理,改进团队运行完成消息显示, 并增强技能激活事件的时间戳记录功能。 refactor(tasks): 简化任务尝试编排器团队执行逻辑 移除团队执行相关代码,将所有任务统一按单步执行处理, 简化任务编排器的复杂度并提升执行效率。 fix(evidence): 修复节点证据评估中需求验证逻辑 更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证, 只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
"""Beaver local tools as real stdio MCP servers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import mcp.types as types
|
|
from mcp.server.lowlevel import Server
|
|
from mcp.server.lowlevel.server import NotificationOptions
|
|
from mcp.server.models import InitializationOptions
|
|
from mcp.server.stdio import stdio_server
|
|
|
|
from beaver.engine.session import SessionManager
|
|
from beaver.memory.curated.store import MemoryStore
|
|
from beaver.services.cron_service import CronService
|
|
from beaver.skills import SkillsLoader
|
|
from beaver.skills.drafts import DraftService
|
|
from beaver.skills.specs import SkillSpecStore
|
|
from beaver.tools.base import BaseTool, ObjectBackedTool, ToolContext
|
|
from beaver.tools.builtins import (
|
|
ClarifyTool,
|
|
CronTool,
|
|
DelegateTool,
|
|
ExecuteCodeTool,
|
|
MemoryTool,
|
|
ProcessTool,
|
|
SendMessageTool,
|
|
SkillManageTool,
|
|
SkillViewTool,
|
|
SkillsListTool,
|
|
SpawnTool,
|
|
TerminalTool,
|
|
TodoTool,
|
|
UserFilesCopyToWorkspaceTool,
|
|
UserFilesListTool,
|
|
UserFilesMkdirTool,
|
|
UserFilesPublishOutputTool,
|
|
UserFilesReadTool,
|
|
UserFilesWriteTool,
|
|
WebFetchTool,
|
|
WebSearchTool,
|
|
WriteFileTool,
|
|
)
|
|
|
|
|
|
LOCAL_TOOL_CATEGORIES = {
|
|
"filesystem": "Beaver Personal Agent Filesystem Tools",
|
|
"runtime": "Beaver Local Runtime Tools",
|
|
"memory": "Beaver Local Memory Tools",
|
|
"skills": "Beaver Local Skills Tools",
|
|
"coordination": "Beaver Local Coordination Tools",
|
|
"scheduler": "Beaver Local Scheduler Tools",
|
|
"web": "Beaver Local Web Tools",
|
|
"team_workflow": "Beaver Local Team Workflow Tools",
|
|
}
|
|
|
|
|
|
def _workspace_path(value: str | None = None) -> Path:
|
|
raw = value or os.getenv("BEAVER_WORKSPACE")
|
|
if raw:
|
|
return Path(raw).expanduser().resolve()
|
|
return Path.cwd()
|
|
|
|
|
|
def _json_content(value: str) -> dict[str, Any]:
|
|
try:
|
|
parsed = json.loads(value)
|
|
return parsed if isinstance(parsed, dict) else {"success": True, "result": parsed}
|
|
except json.JSONDecodeError:
|
|
return {"success": True, "content": value}
|
|
|
|
|
|
def _category_tools(category: str, workspace: Path) -> tuple[list[BaseTool], ToolContext]:
|
|
skill_store = SkillSpecStore(workspace)
|
|
skills_loader = SkillsLoader(workspace, skill_store=skill_store)
|
|
draft_service = DraftService(skill_store)
|
|
services = {
|
|
"skills_loader": skills_loader,
|
|
"draft_service": draft_service,
|
|
}
|
|
context = ToolContext(workspace=str(workspace), services=services)
|
|
|
|
if category == "filesystem":
|
|
tools: list[BaseTool] = [
|
|
ObjectBackedTool(UserFilesListTool()),
|
|
ObjectBackedTool(UserFilesReadTool()),
|
|
ObjectBackedTool(UserFilesWriteTool()),
|
|
ObjectBackedTool(UserFilesMkdirTool()),
|
|
ObjectBackedTool(UserFilesCopyToWorkspaceTool()),
|
|
ObjectBackedTool(UserFilesPublishOutputTool()),
|
|
]
|
|
elif category == "runtime":
|
|
tools = [
|
|
ObjectBackedTool(TerminalTool()),
|
|
ObjectBackedTool(ProcessTool()),
|
|
ObjectBackedTool(ExecuteCodeTool()),
|
|
]
|
|
elif category == "memory":
|
|
session_manager = SessionManager(workspace)
|
|
memory_store = MemoryStore(workspace / "memory" / "curated")
|
|
memory_store.load_from_disk()
|
|
tools = [
|
|
ObjectBackedTool(MemoryTool(store=memory_store)),
|
|
ObjectBackedTool(__import__("beaver.tools.builtins.session_search", fromlist=["SessionSearchTool"]).SessionSearchTool(db=session_manager)),
|
|
]
|
|
elif category == "skills":
|
|
tools = [
|
|
ObjectBackedTool(SkillViewTool(loader=skills_loader)),
|
|
SkillsListTool(),
|
|
SkillManageTool(),
|
|
]
|
|
elif category == "coordination":
|
|
tools = [
|
|
ObjectBackedTool(TodoTool()),
|
|
ObjectBackedTool(ClarifyTool()),
|
|
ObjectBackedTool(DelegateTool()),
|
|
ObjectBackedTool(SpawnTool()),
|
|
ObjectBackedTool(SendMessageTool()),
|
|
]
|
|
elif category == "scheduler":
|
|
services["cron_service"] = CronService(workspace / "cron" / "jobs.json")
|
|
tools = [CronTool()]
|
|
elif category == "web":
|
|
tools = [
|
|
ObjectBackedTool(WebFetchTool()),
|
|
ObjectBackedTool(WebSearchTool()),
|
|
]
|
|
elif category == "team_workflow":
|
|
from beaver.team_workflows.mcp_tools import create_team_workflow_tools
|
|
|
|
tools = create_team_workflow_tools()
|
|
else:
|
|
raise ValueError(f"Unknown local tool category: {category}")
|
|
return tools, context
|
|
|
|
|
|
def create_tools_server(*, category: str, workspace: str | None = None) -> Server:
|
|
workspace_path = _workspace_path(workspace)
|
|
tools, context = _category_tools(category, workspace_path)
|
|
tool_map = {tool.spec.name: tool for tool in tools}
|
|
server = Server(LOCAL_TOOL_CATEGORIES.get(category, f"Beaver Local {category} Tools"))
|
|
|
|
@server.list_tools()
|
|
async def list_tools() -> list[types.Tool]:
|
|
return [
|
|
types.Tool(
|
|
name=tool.spec.name,
|
|
description=tool.spec.description,
|
|
inputSchema=tool.spec.input_schema,
|
|
)
|
|
for tool in tools
|
|
]
|
|
|
|
@server.call_tool(validate_input=True)
|
|
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
tool = tool_map.get(name)
|
|
if tool is None:
|
|
return {"success": False, "error": f"Unknown tool: {name}"}
|
|
result = await tool.invoke(arguments or {}, context)
|
|
if result.raw_output is not None and isinstance(result.raw_output, dict):
|
|
return result.raw_output
|
|
payload = _json_content(result.content)
|
|
if "success" not in payload:
|
|
payload["success"] = bool(result.success)
|
|
if result.error and "error" not in payload:
|
|
payload["error"] = result.error
|
|
return payload
|
|
|
|
return server
|
|
|
|
|
|
async def _run_stdio(category: str, workspace: str | None) -> None:
|
|
server = create_tools_server(category=category, workspace=workspace)
|
|
async with stdio_server() as (read_stream, write_stream):
|
|
await server.run(
|
|
read_stream,
|
|
write_stream,
|
|
InitializationOptions(
|
|
server_name=LOCAL_TOOL_CATEGORIES.get(category, f"beaver-{category}"),
|
|
server_version="0.1.0",
|
|
capabilities=server.get_capabilities(notification_options=NotificationOptions(), experimental_capabilities={}),
|
|
),
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Run a Beaver local tool category as a stdio MCP server.")
|
|
parser.add_argument("--category", choices=sorted(LOCAL_TOOL_CATEGORIES), required=True)
|
|
parser.add_argument("--workspace", default=None)
|
|
args = parser.parse_args()
|
|
asyncio.run(_run_stdio(args.category, args.workspace))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|