117 lines
4.7 KiB
Python
117 lines
4.7 KiB
Python
"""结构化 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}"
|