@hybrd/scheduler 2.0.0
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.
- package/README.md +339 -0
- package/dist/index.cjs +1135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +1096 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +672 -0
- package/src/scheduler.eval.ts +164 -0
- package/src/sql.js.d.ts +21 -0
- package/src/store.ts +316 -0
- package/src/tools.ts +394 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CronJob,
|
|
3
|
+
CronJobCreate,
|
|
4
|
+
CronPayload,
|
|
5
|
+
CronSchedule,
|
|
6
|
+
SessionTarget,
|
|
7
|
+
WakeMode
|
|
8
|
+
} from "@hybrd/types"
|
|
9
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"
|
|
10
|
+
import { z } from "zod"
|
|
11
|
+
import type { SchedulerService } from "./index.js"
|
|
12
|
+
|
|
13
|
+
const ScheduleSchema = z.discriminatedUnion("kind", [
|
|
14
|
+
z.object({
|
|
15
|
+
kind: z.literal("at"),
|
|
16
|
+
at: z.string()
|
|
17
|
+
}),
|
|
18
|
+
z.object({
|
|
19
|
+
kind: z.literal("every"),
|
|
20
|
+
everyMs: z.number().positive(),
|
|
21
|
+
anchorMs: z.number().optional()
|
|
22
|
+
}),
|
|
23
|
+
z.object({
|
|
24
|
+
kind: z.literal("cron"),
|
|
25
|
+
expr: z.string(),
|
|
26
|
+
tz: z.string().optional(),
|
|
27
|
+
staggerMs: z.number().optional()
|
|
28
|
+
})
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const PayloadSchema = z.discriminatedUnion("kind", [
|
|
32
|
+
z.object({
|
|
33
|
+
kind: z.literal("systemEvent"),
|
|
34
|
+
text: z.string()
|
|
35
|
+
}),
|
|
36
|
+
z.object({
|
|
37
|
+
kind: z.literal("agentTurn"),
|
|
38
|
+
message: z.string(),
|
|
39
|
+
model: z.string().optional(),
|
|
40
|
+
thinking: z.string().optional(),
|
|
41
|
+
timeoutSeconds: z.number().optional(),
|
|
42
|
+
allowUnsafeExternalContent: z.boolean().optional()
|
|
43
|
+
})
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const ScheduleTaskSchema = z.object({
|
|
47
|
+
id: z.string().optional(),
|
|
48
|
+
agentId: z.string().optional(),
|
|
49
|
+
sessionKey: z.string().optional(),
|
|
50
|
+
name: z.string(),
|
|
51
|
+
description: z.string().optional(),
|
|
52
|
+
enabled: z.boolean().optional(),
|
|
53
|
+
deleteAfterRun: z.boolean().optional(),
|
|
54
|
+
schedule: ScheduleSchema,
|
|
55
|
+
sessionTarget: z.enum(["main", "isolated"]).optional(),
|
|
56
|
+
wakeMode: z.enum(["now", "next-heartbeat"]).optional(),
|
|
57
|
+
payload: PayloadSchema,
|
|
58
|
+
delivery: z
|
|
59
|
+
.object({
|
|
60
|
+
mode: z.enum(["none", "announce"]),
|
|
61
|
+
channel: z.string().optional(),
|
|
62
|
+
to: z.string().optional(),
|
|
63
|
+
accountId: z.string().optional(),
|
|
64
|
+
bestEffort: z.boolean().optional()
|
|
65
|
+
})
|
|
66
|
+
.optional()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const ListTasksSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
includeDisabled: z.boolean().optional(),
|
|
72
|
+
limit: z.number().optional(),
|
|
73
|
+
offset: z.number().optional(),
|
|
74
|
+
query: z.string().optional(),
|
|
75
|
+
enabled: z.enum(["all", "enabled", "disabled"]).optional(),
|
|
76
|
+
sortBy: z.enum(["nextRunAtMs", "updatedAtMs", "name"]).optional(),
|
|
77
|
+
sortDir: z.enum(["asc", "desc"]).optional()
|
|
78
|
+
})
|
|
79
|
+
.optional()
|
|
80
|
+
|
|
81
|
+
const CancelTaskSchema = z.object({
|
|
82
|
+
taskId: z.string()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const GetTaskSchema = z.object({
|
|
86
|
+
taskId: z.string()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const RunTaskSchema = z.object({
|
|
90
|
+
taskId: z.string(),
|
|
91
|
+
mode: z.enum(["due", "force"]).optional()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
interface SchedulerTool {
|
|
95
|
+
name: string
|
|
96
|
+
description: string
|
|
97
|
+
inputSchema: z.ZodTypeAny
|
|
98
|
+
handler: (args: unknown) => Promise<CallToolResult>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatSchedule(schedule: CronSchedule): string {
|
|
102
|
+
switch (schedule.kind) {
|
|
103
|
+
case "at":
|
|
104
|
+
return `at ${schedule.at}`
|
|
105
|
+
case "every":
|
|
106
|
+
return `every ${schedule.everyMs}ms`
|
|
107
|
+
case "cron":
|
|
108
|
+
return `cron ${schedule.expr}${schedule.tz ? ` (${schedule.tz})` : ""}`
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatJob(job: CronJob): Record<string, unknown> {
|
|
113
|
+
return {
|
|
114
|
+
id: job.id,
|
|
115
|
+
name: job.name,
|
|
116
|
+
description: job.description,
|
|
117
|
+
enabled: job.enabled,
|
|
118
|
+
schedule: formatSchedule(job.schedule),
|
|
119
|
+
sessionTarget: job.sessionTarget,
|
|
120
|
+
wakeMode: job.wakeMode,
|
|
121
|
+
payload: job.payload,
|
|
122
|
+
delivery: job.delivery,
|
|
123
|
+
state: {
|
|
124
|
+
nextRunAtMs: job.state.nextRunAtMs,
|
|
125
|
+
lastRunAtMs: job.state.lastRunAtMs,
|
|
126
|
+
lastRunStatus: job.state.lastRunStatus,
|
|
127
|
+
lastError: job.state.lastError,
|
|
128
|
+
consecutiveErrors: job.state.consecutiveErrors
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createSchedulerTools(
|
|
134
|
+
scheduler: SchedulerService
|
|
135
|
+
): SchedulerTool[] {
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
name: "schedule_task",
|
|
139
|
+
description: `Schedule a task to run at a specific time or interval.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
- One-time: { schedule: { kind: "at", at: "2026-03-01T09:00:00Z" }, payload: { kind: "agentTurn", message: "Check on the project" } }
|
|
143
|
+
- Interval: { schedule: { kind: "every", everyMs: 300000 }, payload: { kind: "agentTurn", message: "Status check" } }
|
|
144
|
+
- Cron: { schedule: { kind: "cron", expr: "0 9 * * 1-5" }, payload: { kind: "agentTurn", message: "Daily standup reminder" } }`,
|
|
145
|
+
inputSchema: ScheduleTaskSchema,
|
|
146
|
+
handler: async (args): Promise<CallToolResult> => {
|
|
147
|
+
const parsed = ScheduleTaskSchema.safeParse(args)
|
|
148
|
+
if (!parsed.success) {
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: JSON.stringify({
|
|
154
|
+
success: false,
|
|
155
|
+
error: parsed.error.message
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
isError: true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const input: CronJobCreate = {
|
|
165
|
+
id: parsed.data.id,
|
|
166
|
+
agentId: parsed.data.agentId,
|
|
167
|
+
sessionKey: parsed.data.sessionKey,
|
|
168
|
+
name: parsed.data.name,
|
|
169
|
+
description: parsed.data.description,
|
|
170
|
+
enabled: parsed.data.enabled,
|
|
171
|
+
deleteAfterRun: parsed.data.deleteAfterRun,
|
|
172
|
+
schedule: parsed.data.schedule as CronSchedule,
|
|
173
|
+
sessionTarget: parsed.data.sessionTarget as
|
|
174
|
+
| SessionTarget
|
|
175
|
+
| undefined,
|
|
176
|
+
wakeMode: parsed.data.wakeMode as WakeMode | undefined,
|
|
177
|
+
payload: parsed.data.payload as CronPayload,
|
|
178
|
+
delivery: parsed.data.delivery
|
|
179
|
+
}
|
|
180
|
+
const job = await scheduler.add(input)
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: JSON.stringify({
|
|
186
|
+
success: true,
|
|
187
|
+
jobId: job.id,
|
|
188
|
+
name: job.name,
|
|
189
|
+
nextRunAtMs: job.state.nextRunAtMs
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: JSON.stringify({
|
|
200
|
+
success: false,
|
|
201
|
+
error:
|
|
202
|
+
err instanceof Error
|
|
203
|
+
? err.message
|
|
204
|
+
: "Failed to schedule task"
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
isError: true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "list_scheduled_tasks",
|
|
215
|
+
description:
|
|
216
|
+
"List all scheduled tasks with optional filtering and pagination",
|
|
217
|
+
inputSchema: ListTasksSchema,
|
|
218
|
+
handler: async (args): Promise<CallToolResult> => {
|
|
219
|
+
const parsed = ListTasksSchema.safeParse(args)
|
|
220
|
+
if (!parsed.success) {
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: JSON.stringify({ error: parsed.error.message })
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
isError: true
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const result = await scheduler.listPage(parsed.data)
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify({
|
|
238
|
+
items: result.items.map(formatJob),
|
|
239
|
+
total: result.total,
|
|
240
|
+
offset: result.offset,
|
|
241
|
+
limit: result.limit,
|
|
242
|
+
hasMore: result.hasMore
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "get_scheduled_task",
|
|
251
|
+
description: "Get details of a specific scheduled task",
|
|
252
|
+
inputSchema: GetTaskSchema,
|
|
253
|
+
handler: async (args): Promise<CallToolResult> => {
|
|
254
|
+
const parsed = GetTaskSchema.safeParse(args)
|
|
255
|
+
if (!parsed.success) {
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: JSON.stringify({ error: parsed.error.message })
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
isError: true
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const job = await scheduler.get(parsed.data.taskId)
|
|
268
|
+
if (!job) {
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: "text",
|
|
273
|
+
text: JSON.stringify({ error: "Task not found" })
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
isError: true
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: JSON.stringify(formatJob(job), null, 2)
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "cancel_scheduled_task",
|
|
291
|
+
description: "Cancel and remove a scheduled task",
|
|
292
|
+
inputSchema: CancelTaskSchema,
|
|
293
|
+
handler: async (args): Promise<CallToolResult> => {
|
|
294
|
+
const parsed = CancelTaskSchema.safeParse(args)
|
|
295
|
+
if (!parsed.success) {
|
|
296
|
+
return {
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: JSON.stringify({
|
|
301
|
+
success: false,
|
|
302
|
+
error: parsed.error.message
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
isError: true
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const result = await scheduler.remove(parsed.data.taskId)
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: JSON.stringify({
|
|
317
|
+
success: true,
|
|
318
|
+
removed: result.removed
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: JSON.stringify({
|
|
329
|
+
success: false,
|
|
330
|
+
error:
|
|
331
|
+
err instanceof Error ? err.message : "Failed to cancel task"
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
],
|
|
335
|
+
isError: true
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "run_scheduled_task",
|
|
342
|
+
description: "Manually trigger a scheduled task to run now",
|
|
343
|
+
inputSchema: RunTaskSchema,
|
|
344
|
+
handler: async (args): Promise<CallToolResult> => {
|
|
345
|
+
const parsed = RunTaskSchema.safeParse(args)
|
|
346
|
+
if (!parsed.success) {
|
|
347
|
+
return {
|
|
348
|
+
content: [
|
|
349
|
+
{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: JSON.stringify({
|
|
352
|
+
success: false,
|
|
353
|
+
error: parsed.error.message
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
isError: true
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const result = await scheduler.run(
|
|
363
|
+
parsed.data.taskId,
|
|
364
|
+
parsed.data.mode
|
|
365
|
+
)
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: JSON.stringify(result)
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: JSON.stringify({
|
|
380
|
+
success: false,
|
|
381
|
+
error:
|
|
382
|
+
err instanceof Error ? err.message : "Failed to run task"
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
],
|
|
386
|
+
isError: true
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export type { SchedulerTool }
|