第一次提交

This commit is contained in:
2026-03-13 16:40:08 +08:00
commit 0a49bcfb2d
277 changed files with 61890 additions and 0 deletions

View File

@ -0,0 +1,116 @@
"""结构化 cron 生命周期控制工具。
cron 任务不是普通用户对话,它经常需要在运行完成后主动告诉调度器:
- 这个任务已经可以删掉;
- 今天这一轮先结束,下一天再继续;
- 下次应该改成新的时间表。
这个工具就是让模型把这些决策显式写成结构化数据,而不是只留在自然语言里。
"""
from __future__ import annotations
from typing import Any
from nanobot.agent.tools.base import Tool
from nanobot.cron.types import CronAction
class CronActionTool(Tool):
"""捕获模型输出的机器可读 cron 控制决策。"""
def __init__(self, job_id: str):
# `job_id` 仅用于回显和审计,不参与决策本身。
self.job_id = job_id
# `_decision` 在本轮 agent 执行期间最多被写一次,外部在结束后读取。
self._decision: CronAction | None = None
@property
def name(self) -> str:
return "cron_action"
@property
def description(self) -> str:
return "Record a structured lifecycle action for the currently running cron job."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["none", "remove", "disable", "complete_today", "reschedule"],
"description": "Lifecycle action for the current cron job",
},
"reason": {
"type": "string",
"description": "Short reason for audit logs",
},
"every_seconds": {
"type": "integer",
"description": "Required when action=reschedule and using fixed interval",
},
"cron_expr": {
"type": "string",
"description": "Required when action=reschedule and using cron expression",
},
"tz": {
"type": "string",
"description": "Optional timezone for cron_expr reschedules",
},
"at": {
"type": "string",
"description": "Required when action=reschedule and using one-time ISO datetime",
},
},
"required": ["action"],
}
@property
def decision(self) -> CronAction | None:
# 暴露最终结构化决策给 cron runtime便于后处理调度状态。
return self._decision
async def execute(
self,
action: str,
reason: str | None = None,
every_seconds: int | None = None,
cron_expr: str | None = None,
tz: str | None = None,
at: str | None = None,
**_kwargs: Any,
) -> str:
# 统一做小写规范化,避免模型传入 `Remove` / `REMOVE` 之类大小写变体。
normalized = (action or "").strip().lower()
allowed_actions = {"none", "remove", "disable", "complete_today", "reschedule"}
if normalized not in allowed_actions:
return f"Error: unsupported cron action '{action}'"
# 非重排任务不允许额外携带调度字段,避免出现“说 remove 但又传 cron_expr”的脏数据。
if normalized != "reschedule" and any(value is not None for value in (every_seconds, cron_expr, tz, at)):
return "Error: schedule fields can only be used when action='reschedule'"
if normalized == "reschedule":
# 重新排期必须在三种时间表达方式里三选一,不能都不传,也不能混传。
options = int(every_seconds is not None) + int(bool(cron_expr)) + int(bool(at))
if options != 1:
return "Error: reschedule requires exactly one of every_seconds, cron_expr, or at"
# 时区只有 cron 表达式才有意义。
if tz and not cron_expr:
return "Error: tz can only be used with cron_expr"
# 校验通过后,把本轮决策固化为 dataclass交给 runtime 在执行后统一消费。
self._decision = CronAction(
action=normalized or "none",
reason=(reason or "").strip() or None,
every_seconds=every_seconds,
cron_expr=cron_expr,
tz=tz,
at=at,
)
# 返回给模型/日志的是一条可读确认文本,方便工具调用结果出现在上下文里。
detail = f" for job {self.job_id}"
if self._decision.reason:
detail += f" ({self._decision.reason})"
return f"Recorded cron_action={self._decision.action}{detail}"