Files
beaver_project/app-instance/backend/beaver/foundation/models/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

267 lines
10 KiB
Python

"""Scheduled task models for Beaver cron.
The scheduler borrows Hermes' durable JSON + explicit schedule parsing shape,
but the execution target is Beaver Task mode: every trigger creates a normal
Task run instead of a detached agent turn.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
from uuid import uuid4
CronScheduleKind = Literal["at", "every", "cron"]
CronPayloadKind = Literal["agent_turn", "system_event"]
CronPayloadMode = Literal["notification", "task"]
@dataclass(slots=True)
class CronSchedule:
kind: CronScheduleKind
at_ms: int | None = None
every_ms: int | None = None
expr: str | None = None
tz: str | None = None
display: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"kind": self.kind,
"at_ms": self.at_ms,
"every_ms": self.every_ms,
"expr": self.expr,
"tz": self.tz,
"display": self.display,
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "CronSchedule":
return cls(
kind=str(payload.get("kind") or "every"), # type: ignore[arg-type]
at_ms=_optional_int(payload.get("at_ms") or payload.get("atMs")),
every_ms=_optional_int(payload.get("every_ms") or payload.get("everyMs")),
expr=_optional_str(payload.get("expr")),
tz=_optional_str(payload.get("tz")),
display=_optional_str(payload.get("display")),
)
@dataclass(slots=True)
class CronPayload:
kind: CronPayloadKind = "agent_turn"
mode: CronPayloadMode = "notification"
message: str = ""
session_key: str | None = None
requires_followup: bool = False
deliver: bool = False
channel: str | None = None
to: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"kind": self.kind,
"mode": self.mode,
"message": self.message,
"session_key": self.session_key,
"requires_followup": self.requires_followup,
"deliver": self.deliver,
"channel": self.channel,
"to": self.to,
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "CronPayload":
return cls(
kind=str(payload.get("kind") or "agent_turn"), # type: ignore[arg-type]
mode=_payload_mode(payload.get("mode"), default="task"),
message=str(payload.get("message") or ""),
session_key=_optional_str(payload.get("session_key") or payload.get("sessionKey")),
requires_followup=bool(payload.get("requires_followup") or payload.get("requiresFollowup") or False),
deliver=bool(payload.get("deliver", False)),
channel=_optional_str(payload.get("channel")),
to=_optional_str(payload.get("to")),
)
@dataclass(slots=True)
class CronRunRecord:
started_at_ms: int
scheduled_run_id: str = field(default_factory=lambda: uuid4().hex)
finished_at_ms: int | None = None
status: Literal["running", "ok", "error", "skipped"] = "running"
mode: CronPayloadMode = "notification"
notification_session_id: str | None = None
output: str | None = None
task_id: str | None = None
run_id: str | None = None
error: str | None = None
engaged: bool = False
engaged_at_ms: int | None = None
engage_intent: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"scheduled_run_id": self.scheduled_run_id,
"started_at_ms": self.started_at_ms,
"finished_at_ms": self.finished_at_ms,
"status": self.status,
"mode": self.mode,
"notification_session_id": self.notification_session_id,
"output": self.output,
"task_id": self.task_id,
"run_id": self.run_id,
"error": self.error,
"engaged": self.engaged,
"engaged_at_ms": self.engaged_at_ms,
"engage_intent": self.engage_intent,
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "CronRunRecord":
return cls(
scheduled_run_id=str(payload.get("scheduled_run_id") or payload.get("scheduledRunId") or uuid4().hex),
started_at_ms=int(payload.get("started_at_ms") or payload.get("startedAtMs") or 0),
finished_at_ms=_optional_int(payload.get("finished_at_ms") or payload.get("finishedAtMs")),
status=str(payload.get("status") or "running"), # type: ignore[arg-type]
mode=_payload_mode(payload.get("mode"), default="notification"),
notification_session_id=_optional_str(payload.get("notification_session_id") or payload.get("notificationSessionId")),
output=_optional_str(payload.get("output")),
task_id=_optional_str(payload.get("task_id") or payload.get("taskId")),
run_id=_optional_str(payload.get("run_id") or payload.get("runId")),
error=_optional_str(payload.get("error")),
engaged=bool(payload.get("engaged", False)),
engaged_at_ms=_optional_int(payload.get("engaged_at_ms") or payload.get("engagedAtMs")),
engage_intent=_optional_str(payload.get("engage_intent") or payload.get("engageIntent")),
)
@dataclass(slots=True)
class CronJob:
id: str
name: str
enabled: bool
schedule: CronSchedule
payload: CronPayload
created_at_ms: int
updated_at_ms: int
next_run_at_ms: int | None = None
last_run_at_ms: int | None = None
last_status: Literal["ok", "error", "skipped"] | None = None
last_error: str | None = None
delete_after_run: bool = False
history: list[CronRunRecord] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"enabled": self.enabled,
"schedule": self.schedule.to_dict(),
"payload": self.payload.to_dict(),
"created_at_ms": self.created_at_ms,
"updated_at_ms": self.updated_at_ms,
"next_run_at_ms": self.next_run_at_ms,
"last_run_at_ms": self.last_run_at_ms,
"last_status": self.last_status,
"last_error": self.last_error,
"delete_after_run": self.delete_after_run,
"history": [item.to_dict() for item in self.history],
}
def to_api_dict(self) -> dict[str, Any]:
latest = self.history[-1] if self.history else None
return {
"id": self.id,
"name": self.name,
"enabled": self.enabled,
"schedule_kind": self.schedule.kind,
"schedule_display": self.schedule.display or _schedule_display(self.schedule),
"schedule_expr": self.schedule.expr,
"schedule_every_ms": self.schedule.every_ms,
"message": self.payload.message,
"mode": self.payload.mode,
"requires_followup": self.payload.requires_followup,
"deliver": self.payload.deliver,
"channel": self.payload.channel,
"to": self.payload.to,
"session_key": self.payload.session_key,
"next_run_at_ms": self.next_run_at_ms,
"last_run_at_ms": self.last_run_at_ms,
"last_status": self.last_status,
"last_error": self.last_error,
"last_scheduled_run_id": latest.scheduled_run_id if latest else None,
"last_task_id": latest.task_id if latest else None,
"last_run_id": latest.run_id if latest else None,
"history": [item.to_dict() for item in self.history],
"created_at_ms": self.created_at_ms,
"updated_at_ms": self.updated_at_ms,
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "CronJob":
schedule_payload = payload.get("schedule") if isinstance(payload.get("schedule"), dict) else {}
payload_payload = payload.get("payload") if isinstance(payload.get("payload"), dict) else {}
return cls(
id=str(payload["id"]),
name=str(payload.get("name") or payload["id"]),
enabled=bool(payload.get("enabled", True)),
schedule=CronSchedule.from_dict(schedule_payload),
payload=CronPayload.from_dict(payload_payload),
created_at_ms=int(payload.get("created_at_ms") or payload.get("createdAtMs") or 0),
updated_at_ms=int(payload.get("updated_at_ms") or payload.get("updatedAtMs") or 0),
next_run_at_ms=_optional_int(payload.get("next_run_at_ms") or payload.get("nextRunAtMs")),
last_run_at_ms=_optional_int(payload.get("last_run_at_ms") or payload.get("lastRunAtMs")),
last_status=_optional_str(payload.get("last_status") or payload.get("lastStatus")), # type: ignore[arg-type]
last_error=_optional_str(payload.get("last_error") or payload.get("lastError")),
delete_after_run=bool(payload.get("delete_after_run") or payload.get("deleteAfterRun") or False),
history=[
CronRunRecord.from_dict(item)
for item in payload.get("history") or []
if isinstance(item, dict)
],
)
@dataclass(slots=True)
class CronExecutionResult:
response: str | None = None
task_id: str | None = None
run_id: str | None = None
notification_session_id: str | None = None
mode: CronPayloadMode = "notification"
def _schedule_display(schedule: CronSchedule) -> str:
if schedule.kind == "every":
seconds = int((schedule.every_ms or 0) / 1000)
return f"every {seconds}s"
if schedule.kind == "cron":
return schedule.expr or "cron"
return "one-time"
def _optional_str(value: Any) -> str | None:
if value in (None, ""):
return None
return str(value)
def _optional_int(value: Any) -> int | None:
if value in (None, ""):
return None
def _payload_mode(value: Any, *, default: CronPayloadMode = "notification") -> CronPayloadMode:
if value in (None, ""):
return default
cleaned = str(value or "").strip().lower()
if cleaned == "task":
return "task"
return "notification"
try:
return int(value)
except (TypeError, ValueError):
return None