@realqiyan/agent-task 0.1.0-rc.3 → 0.1.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { absolutePath, identifier, timestamp } from "../shared/validation.js";
3
- import { nodeData, nodeStatusSchema, questionData, runStatusSchema, taskData, taskStatusSchema } from "../tasks/validation.js";
3
+ import { cronScheduleData, nodeData, nodeStatusSchema, questionData, runStatusSchema, taskData, taskStatusSchema } from "../tasks/validation.js";
4
4
  /** Shared MCP initialization text; both the HTTP server and the local stdio proxy advertise it. */
5
5
  export const serverInstructions = `Agent Task runs caller-defined DAGs asynchronously with local ACP executors. A task is a persistent definition and schedule; a run is one execution. Task and run UUIDs are distinct from caller-defined DAG node IDs.
6
6
  Check service_status and executor_list before task_create. Poll task_run_list(taskId) at intervals for progress; use task_run_get for full outputs and task_run_reply for requested user facts. Creation returns a task, not a run or completed result. Disconnecting does not cancel work.
@@ -17,11 +17,10 @@ const scheduleOutput = z.discriminatedUnion("type", [
17
17
  type: z.literal("once"),
18
18
  at: timestamp.describe("Scheduled trigger, not actual execution start."),
19
19
  }).strict(),
20
- z.object({
21
- type: z.literal("interval"),
22
- startAt: timestamp.describe("Anchor for the repeating schedule."),
23
- seconds: z.number().int().describe("Fixed elapsed seconds between triggers."),
24
- }).strict(),
20
+ cronScheduleData.safeExtend({
21
+ expression: cronScheduleData.shape.expression.describe("Five-field calendar Cron expression."),
22
+ timezone: cronScheduleData.shape.timezone.describe("Explicit timezone used to calculate calendar triggers."),
23
+ }),
25
24
  ]).describe("Stored schedule. Task status and nextAt are authoritative for future eligibility.");
26
25
  const taskNodeOutput = nodeData.safeExtend({
27
26
  id: nodeData.shape.id.describe("Caller-defined DAG node ID within this task; not a node-run UUID."),
@@ -39,7 +39,7 @@ export function createMcpServer(service, fatal) {
39
39
  }
40
40
  register("service_status", "Check scheduler/storage health, coordinator configuration and capacity before creating work or when diagnosing a stalled run. Does not test model credentials or availability; use executor_list for executor readiness.", z.object({}).strict(), statusOutput, () => service.supervisor.status());
41
41
  register("executor_list", "Find local ACP executor IDs for task_create and inspect adapter preparation. Select only state=ready; this confirms launch dependencies, not native authentication or tool authorization. Does not refresh discovery or install adapters.", z.object({}).strict(), executorsOutput, () => ({ executors: service.executors.list().executors.map(executorStatus) }));
42
- register("task_create", "Create a persistent task for immediate, one-time or interval execution. Supply the complete fixed DAG and a ready executor per node; the coordinator reviews deliveries and requests missing facts, but does not plan or reassign work. Requires service_status.coordinatorConfigured=true. Returns a task, not an execution result; find runs with task_run_list(taskId). Background model/ACP work survives MCP disconnection. Retry the same submission with the same requestKey and parameters, not a replacement task.", taskSchema, taskOutput, input => taskView(service.definitions.create(input)), { readOnly: false, destructive: false, idempotent: true, openWorld: true });
42
+ register("task_create", "Create a persistent task for immediate, one-time or Cron-scheduled execution. Supply the complete fixed DAG and a ready executor per node; the coordinator reviews deliveries and requests missing facts, but does not plan or reassign work. Requires service_status.coordinatorConfigured=true. Returns a task, not an execution result; find runs with task_run_list(taskId). Background model/ACP work survives MCP disconnection. Retry the same submission with the same requestKey and parameters, not a replacement task.", taskSchema, taskOutput, input => taskView(service.definitions.create(input)), { readOnly: false, destructive: false, idempotent: true, openWorld: true });
43
43
  register("task_list", "Find tasks and their future-trigger state, including cancelled and completed tasks. Returns compact records, oldest first; follow next for more. Use task_get for the full definition and task_run_list for execution outcomes.", taskListSchema, pageOutput(taskSummaryOutput), input => {
44
44
  const page = service.definitions.list(input.after, input.limit);
45
45
  return { items: page.items.map(taskSummary), next: page.next };
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { requestTimestamp } from "../shared/validation.js";
3
- import { nodeData, replyData, runStatusSchema, taskData, taskUpdateData } from "../tasks/validation.js";
3
+ import { cronScheduleData, nodeData, replyData, runStatusSchema, taskData, taskUpdateData } from "../tasks/validation.js";
4
4
  export const nodeSchema = nodeData.extend({
5
5
  id: nodeData.shape.id.describe("Unique caller-defined ID in this DAG, referenced by dependsOn; not a run UUID."),
6
6
  prompt: z.string().trim().min(1).max(64_000).describe("Self-contained work and acceptance criteria for this executor. The task-level prompt is not copied here. Plain text/Markdown delivery is valid unless a format is required."),
@@ -26,7 +26,7 @@ export const taskRunListSchema = taskListSchema.extend({
26
26
  });
27
27
  export const taskUpdateSchema = taskUpdateData.extend({
28
28
  id: taskIdSchema.shape.id,
29
- action: taskUpdateData.shape.action.describe("pause keeps the next trigger but suspends it; resume re-enables it without catch-up (past interval slots advance; past now/once slots become completed); cancel permanently removes future triggers. pause/resume require a remaining trigger. resume fails if any run has an interrupted node."),
29
+ action: taskUpdateData.shape.action.describe("pause keeps the next trigger but suspends it; resume re-enables it without catch-up (past Cron slots advance; past now/once slots become completed); cancel permanently removes future triggers. pause/resume require a remaining trigger. resume fails if any run has an interrupted node."),
30
30
  });
31
31
  export const taskSchema = taskData.extend({
32
32
  requestKey: taskData.shape.requestKey.describe("Caller-chosen creation key, scoped to the canonical workspace. Keep it stable for retries: identical normalized parameters return the existing task, including cancelled/completed tasks; changes conflict. A new key creates separate work, not a retry."),
@@ -40,11 +40,11 @@ export const taskSchema = taskData.extend({
40
40
  type: z.literal("once").describe("One slot at the specified future time."),
41
41
  at: requestTimestamp.describe("Future ISO 8601 trigger time with UTC Z or numeric offset; normalized to UTC."),
42
42
  }).strict(),
43
- z.object({
44
- type: z.literal("interval").describe("Repeating slots anchored to startAt, not to completion of the previous run."),
45
- startAt: requestTimestamp.describe("First trigger, in the future, as ISO 8601 with UTC Z or numeric offset."),
46
- seconds: z.number().int().min(60).max(31_536_000).describe("Fixed elapsed seconds between slots; not a calendar/timezone schedule."),
47
- }).strict(),
43
+ cronScheduleData.safeExtend({
44
+ type: z.literal("cron").describe("Repeating calendar slots; the first match strictly after creation, not after the previous run completes."),
45
+ expression: z.string().trim().min(1).max(200).describe("Five fields: minute hour day-of-month month day-of-week, e.g. 0 9 * * 1-5. No seconds, @ aliases or hashed H fields. Day-of-month and day-of-week use OR when both are restricted."),
46
+ timezone: z.string().trim().min(1).max(100).describe("Required IANA timezone, e.g. Asia/Shanghai or UTC; never inferred from the server environment. DST follows cron-parser calendar rules."),
47
+ }),
48
48
  ]).describe("When runs become eligible, not guaranteed start times. Slots over 30 seconds late or overlapping a running/waiting run of this task are skipped, not queued for catch-up."),
49
49
  expiresAt: requestTimestamp.optional().describe("Cutoff for new runs and previously unstarted nodes, not a completion timeout. Must follow the first trigger. Existing ACP sessions may continue. ISO 8601 with timezone; omit for no cutoff."),
50
50
  maxRunSeconds: taskData.shape.maxRunSeconds.default(1800).describe("Run execution budget in seconds from coordinator start, excluding human-input waits; includes ACP execution. Separate from expiresAt and each node's session lifetime."),
@@ -0,0 +1,10 @@
1
+ import { CronExpressionParser } from "cron-parser";
2
+ /** Five calendar fields, deterministic expressions and an explicit IANA timezone. */
3
+ export function parseCron(schedule, now) {
4
+ if (schedule.expression.trim().split(/\s+/).length !== 5 || /\bH\b/i.test(schedule.expression)) {
5
+ throw new Error("Cron requires five fields (minute hour day-of-month month day-of-week); hashed fields are not supported");
6
+ }
7
+ new Intl.DateTimeFormat("en", { timeZone: schedule.timezone });
8
+ return CronExpressionParser.parse(schedule.expression, { currentDate: now, tz: schedule.timezone });
9
+ }
10
+ //# sourceMappingURL=cron.js.map
@@ -1,9 +1,10 @@
1
1
  import { TaskError } from "../shared/errors.js";
2
+ import { parseCron } from "./cron.js";
2
3
  export function hasUncertainOutcome(run) {
3
4
  return run.nodes.some(node => node.status === "interrupted");
4
5
  }
5
6
  export function initialTime(schedule, now) {
6
- return schedule.type === "now" ? now : Date.parse(schedule.type === "once" ? schedule.at : schedule.startAt);
7
+ return schedule.type === "now" ? now : schedule.type === "once" ? Date.parse(schedule.at) : parseCron(schedule, now).next().getTime();
7
8
  }
8
9
  export function resumeSchedule(task, now) {
9
10
  const next = structuredClone(task);
@@ -11,8 +12,8 @@ export function resumeSchedule(task, now) {
11
12
  throw new TaskError("precondition", "Task has no future schedule");
12
13
  next.status = "active";
13
14
  if (next.nextAt < now) {
14
- if (next.input.schedule.type === "interval")
15
- next.nextAt += (Math.floor((now - next.nextAt) / (next.input.schedule.seconds * 1000)) + 1) * next.input.schedule.seconds * 1000;
15
+ if (next.input.schedule.type === "cron")
16
+ next.nextAt = parseCron(next.input.schedule, now).next().getTime();
16
17
  else {
17
18
  next.nextAt = null;
18
19
  next.status = "completed";
@@ -29,9 +30,17 @@ export function dueTrigger(task, overlaps, now) {
29
30
  if (task.nextAt > now)
30
31
  return { kind: "none" };
31
32
  const schedule = task.input.schedule;
32
- const count = schedule.type === "interval" ? Math.floor((now - task.nextAt) / (schedule.seconds * 1000)) + 1 : 1;
33
- const at = task.nextAt + (schedule.type === "interval" ? (count - 1) * schedule.seconds * 1000 : 0);
34
- return { kind: "due", at, count, nextAt: schedule.type === "interval" ? at + schedule.seconds * 1000 : null,
33
+ let count = 1, at = task.nextAt, nextAt = null;
34
+ if (schedule.type === "cron") {
35
+ const cron = parseCron(schedule, at);
36
+ nextAt = cron.next().getTime();
37
+ while (nextAt <= now) {
38
+ at = nextAt;
39
+ count++;
40
+ nextAt = cron.next().getTime();
41
+ }
42
+ }
43
+ return { kind: "due", at, count, nextAt,
35
44
  reason: overlaps ? "Previous round still active; slot skipped" : now - at > 30_000 ? "Late slot skipped" : null };
36
45
  }
37
46
  //# sourceMappingURL=schedule.js.map
@@ -1,9 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { absolutePath, identifier, text, timestamp } from "../shared/validation.js";
3
+ import { parseCron } from "./cron.js";
4
+ export const cronScheduleData = z.object({
5
+ type: z.literal("cron"), expression: text(200), timezone: text(100),
6
+ }).strict().superRefine((schedule, ctx) => {
7
+ try {
8
+ parseCron(schedule, Date.UTC(2000, 0, 1)).next();
9
+ }
10
+ catch {
11
+ ctx.addIssue({ code: "custom", message: "Invalid five-field Cron expression or timezone" });
12
+ }
13
+ });
3
14
  export const scheduleData = z.discriminatedUnion("type", [
4
15
  z.object({ type: z.literal("now") }).strict(),
5
16
  z.object({ type: z.literal("once"), at: timestamp }).strict(),
6
- z.object({ type: z.literal("interval"), startAt: timestamp, seconds: z.number().int().min(60).max(31_536_000) }).strict(),
17
+ cronScheduleData,
7
18
  ]);
8
19
  export const taskStatusSchema = z.enum(["active", "paused", "cancelled", "completed", "expired"]);
9
20
  export const runStatusSchema = z.enum(["running", "succeeded", "failed", "interrupted", "skipped", "cancelled", "timed_out", "waiting_input"]);
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 产品目标
8
8
 
9
- - 调用方通过 MCP 提交整体目标、DAG 和每节点的 ACP 执行端指派,支持立即、定时及周期执行。
9
+ - 调用方通过 MCP 提交整体目标、DAG 和每节点的 ACP 执行端指派,支持立即、定时及 Cron 周期执行;Cron 使用五段表达式与显式时区,契约见 [task-contract.ts](../src/protocol/task-contract.ts)。
10
10
  - 任务不依赖发起客户端持续在线,保留执行、交付、失败和缺少用户事实的记录。
11
11
  - 初始化由用户确认目录、端口、令牌及协调模型;执行端自动发现并按需准备适配器。stdio 接入连接独立后台,宿主退出后后台继续调度;宿主配置由用户添加,不自动识别宿主当前项目。
12
12
  - 协调员推进既定 DAG、解释中间结果、处理澄清并组织交付;领域分析和工具操作交给执行端。
@@ -45,7 +45,7 @@ ACP 执行端:原生会话、领域推理和工具操作
45
45
 
46
46
  当前是调研和 MVP 验证阶段,直接调整接口和数据结构,不维护旧版兼容或业务库版本递增;这不授权迁移或删除历史数据。
47
47
 
48
- 暂不实现:执行中修改 DAG、自动重试或跨端故障转移、丢失 ACP 会话的重连、人工核对后强制恢复、远程执行、多租户、日历时区调度、技能同步及动态 MCP 转发。它们需要独立产品决策,不因引入 durable 自动获得。
48
+ 暂不实现:执行中修改 DAG、自动重试或跨端故障转移、丢失 ACP 会话的重连、人工核对后强制恢复、远程执行、多租户、技能同步及动态 MCP 转发。它们需要独立产品决策,不因引入 durable 自动获得。
49
49
 
50
50
  启动方式见[部署说明](deployment.md)。[本机控制台](web-console.md)复用任务服务,使用独立管理员认证与浏览器请求边界;不提供文件下载或其他任务写操作。
51
51
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  | 责任 | 实现入口 | 基础验证 |
10
10
  | --- | --- | --- |
11
- | 任务定义、执行端快照、未来触发控制 | [TaskDefinitions](../src/tasks/definitions.ts)、[图校验](../src/tasks/graph.ts)、[时间规则](../src/tasks/schedule.ts) | [tasks.test.mjs](../tests/tasks.test.mjs) |
11
+ | 任务定义、执行端快照、未来触发控制 | [TaskDefinitions](../src/tasks/definitions.ts)、[图校验](../src/tasks/graph.ts)、[时间规则](../src/tasks/schedule.ts)、[Cron 解析](../src/tasks/cron.ts) | [tasks.test.mjs](../tests/tasks.test.mjs)、[schedule.test.mjs](../tests/schedule.test.mjs) |
12
12
  | Run 与节点状态的业务修改入口、派发与提交顺序 | [TaskRuns](../src/tasks/runs.ts)、[数据模型](../src/tasks/model.ts)、[窄能力契约](../src/tasks/ports.ts) | [tasks.test.mjs](../tests/tasks.test.mjs)、[task-storage.test.mjs](../tests/task-storage.test.mjs) |
13
13
  | 时钟、协调会话监督、取消与收尾 | [RunSupervisor](../src/tasks/supervisor.ts)、[NodeSessions](../src/tasks/sessions.ts) | [tasks.test.mjs](../tests/tasks.test.mjs)、[restart.test.mjs](../tests/restart.test.mjs) |
14
14
  | 人工事实等待与回复 | [纯状态规则](../src/tasks/human-input.ts)、[TaskRuns](../src/tasks/runs.ts) | [human-input.test.mjs](../tests/human-input.test.mjs) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realqiyan/agent-task",
3
- "version": "0.1.0-rc.3",
3
+ "version": "0.1.0-rc.4",
4
4
  "license": "MIT",
5
5
  "author": "realqiyan",
6
6
  "repository": {
@@ -61,6 +61,7 @@
61
61
  "@earendil-works/pi-ai": "1.3.0",
62
62
  "@earendil-works/pi-durable": "1.3.0",
63
63
  "@modelcontextprotocol/sdk": "1.32.1",
64
+ "cron-parser": "5.10.2",
64
65
  "express": "5.3.0",
65
66
  "zod": "4.6.7"
66
67
  },