@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/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 }