Files
beaver_project/app-instance/backend/beaver/tools/builtins/cron.py
steven_li 30ab74ffb2 feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接
- 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、
  PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、
  TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等
- 实现工具注册和装配功能
- 添加技能选择上下文参数
- 支持思考模式控制参数thinking_enabled

feat(coordinator): 重构任务执行计划器参数命名

- 将learning_candidate_enabled重命名为allow_candidate_generation
- 更新TeamGraphScheduler中的参数传递
- 修改LocalAgentRunner中的相关参数处理
- 更新README文档中的相应描述

refactor(context): 标准化工具调用参数格式

- 添加_json导入用于参数序列化
- 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷
- 修复工具调用中参数非字符串类型的序列化问题

refactor(session): 优化消息历史记录过滤逻辑

- 修改get_messages_as_conversation为基于运行状态过滤消息
- 排除未完成、失败或错误结束的运行记录
- 改进对话历史的可见性控制机制

fix(store): 修复FTS索引重建逻辑

- 添加异常处理防止FTS索引创建失败
- 实现_rebuild_fts_index方法重新构建全文搜索索引
- 优化索引触发器和表的维护流程
2026-05-14 09:43:48 +08:00

164 lines
6.3 KiB
Python

"""Built-in cron tool for managing scheduled Beaver Tasks."""
from __future__ import annotations
import json
from typing import Any
from beaver.services.cron_service import CronService, schedule_from_api
from beaver.tools.base import BaseTool, ToolContext, ToolResult, ToolSpec
CRON_TOOL_DESCRIPTION = (
"Create and manage scheduled Beaver notifications or Tasks. Notification mode "
"sends scheduled results to the fixed notification session; task mode creates "
"a Task run. Actions: add, list, remove, toggle, run."
)
CRON_TOOL_PARAMETERS: dict[str, Any] = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["add", "list", "remove", "toggle", "run"],
"description": "The scheduled-task operation to perform.",
},
"name": {
"type": "string",
"description": "Short scheduled-task name. Optional for add.",
},
"message": {
"type": "string",
"description": "The task instruction to run when the schedule triggers. Required for add.",
},
"schedule": {
"type": "string",
"description": "Hermes-style schedule, for example 'every 15m', '0 9 * * *', or an ISO datetime.",
},
"every_seconds": {
"type": "integer",
"minimum": 1,
"description": "Fixed interval in seconds for recurring scheduled tasks.",
},
"cron_expr": {
"type": "string",
"description": "Cron expression such as '0 9 * * *'.",
},
"tz": {
"type": "string",
"description": "IANA timezone for cron_expr, for example 'Asia/Shanghai'.",
},
"at_iso": {
"type": "string",
"description": "ISO datetime for one-time scheduled tasks.",
},
"job_id": {
"type": "string",
"description": "Scheduled-task ID for remove, toggle, or run.",
},
"enabled": {
"type": "boolean",
"description": "Whether the scheduled task should be enabled when action is toggle.",
},
"mode": {
"type": "string",
"enum": ["notification", "task"],
"description": "Use notification for reminders/reports; use task only when the scheduled work requires Task tracking.",
},
"requires_followup": {
"type": "boolean",
"description": "Whether a task-mode scheduled run should appear as an active task awaiting user follow-up.",
},
},
"required": ["action"],
}
class CronTool(BaseTool):
"""Tool-facing wrapper around the process CronService."""
@property
def spec(self) -> ToolSpec:
return ToolSpec(
name="cron",
description=CRON_TOOL_DESCRIPTION,
input_schema=CRON_TOOL_PARAMETERS,
toolset="cron",
always_available=False,
)
async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult:
try:
result = await self._invoke(arguments, context)
return ToolResult(
success=bool(result.get("success", True)),
content=json.dumps(result, ensure_ascii=False),
tool_name=self.spec.name,
error=str(result.get("error")) if result.get("error") else None,
raw_output=result,
)
except Exception as exc:
return ToolResult(
success=False,
content=json.dumps({"success": False, "error": str(exc)}, ensure_ascii=False),
tool_name=self.spec.name,
error=str(exc),
)
async def _invoke(self, arguments: dict[str, Any], context: ToolContext) -> dict[str, Any]:
service = self._resolve_cron_service(context)
action = str(arguments.get("action") or "").strip().lower()
if action == "add":
schedule = schedule_from_api(arguments)
job = service.add_job(
name=str(arguments.get("name") or "").strip(),
message=str(arguments.get("message") or "").strip(),
schedule=schedule,
session_key=str(arguments.get("session_key") or context.session_id or "").strip() or None,
payload_kind="agent_turn",
mode=str(arguments.get("mode") or "notification").strip().lower(),
requires_followup=bool(arguments.get("requires_followup", False)),
)
return {"success": True, "job": job.to_api_dict()}
if action == "list":
include_disabled = bool(arguments.get("include_disabled", True))
return {
"success": True,
"jobs": [job.to_api_dict() for job in service.list_jobs(include_disabled=include_disabled)],
}
if action == "remove":
job_id = _required_job_id(arguments)
return {"success": service.remove_job(job_id), "job_id": job_id}
if action == "toggle":
job_id = _required_job_id(arguments)
job = service.update_enabled(job_id, bool(arguments.get("enabled", True)))
if job is None:
return {"success": False, "error": f"Scheduled task {job_id!r} was not found."}
return {"success": True, "job": job.to_api_dict()}
if action == "run":
job_id = _required_job_id(arguments)
ok = await service.run_job(job_id, force=True)
job = service.get_job(job_id)
return {
"success": ok,
"job_id": job_id,
"job": job.to_api_dict() if job is not None else None,
}
return {"success": False, "error": "action must be one of: add, list, remove, toggle, run"}
@staticmethod
def _resolve_cron_service(context: ToolContext) -> CronService:
service = context.get("cron_service")
if isinstance(service, CronService):
return service
if not context.workspace:
raise RuntimeError("Cron service is unavailable for this runtime.")
return CronService(f"{context.workspace}/cron/jobs.json")
def _required_job_id(arguments: dict[str, Any]) -> str:
job_id = str(arguments.get("job_id") or "").strip()
if not job_id:
raise ValueError("job_id is required")
return job_id