@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/README.md
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# @hybrd/scheduler
|
|
2
|
+
|
|
3
|
+
Agentic scheduling system for Hybrid AI agents. **100% OpenClaw-compatible** with additional features for agent-native workflows.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The scheduler enables agents to schedule future actions for themselves. This is "agentic scheduling" - the LLM decides when and what to schedule, not a human.
|
|
8
|
+
|
|
9
|
+
## OpenClaw Compatibility
|
|
10
|
+
|
|
11
|
+
This scheduler implements the complete OpenClaw CronService API:
|
|
12
|
+
|
|
13
|
+
### Schedule Types
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
type CronSchedule =
|
|
17
|
+
| { kind: "at"; at: string } // One-time at specific time
|
|
18
|
+
| { kind: "every"; everyMs: number; anchorMs?: number } // Interval with anchor
|
|
19
|
+
| { kind: "cron"; expr: string; tz?: string; staggerMs?: number } // Cron expression
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Core API
|
|
23
|
+
|
|
24
|
+
| Method | Description |
|
|
25
|
+
|--------|-------------|
|
|
26
|
+
| `start()` | Start the scheduler, clear stale markers, arm timer |
|
|
27
|
+
| `stop()` | Stop the scheduler |
|
|
28
|
+
| `status()` | Get scheduler status (enabled, job count, next wake) |
|
|
29
|
+
| `list()` | List all jobs (sorted by next run) |
|
|
30
|
+
| `listPage(opts)` | Paginated list with query, sort, offset |
|
|
31
|
+
| `add(input)` | Create a new scheduled job |
|
|
32
|
+
| `update(id, patch)` | Update job properties |
|
|
33
|
+
| `remove(id)` | Delete a job |
|
|
34
|
+
| `run(id, mode)` | Manually trigger a job |
|
|
35
|
+
| `get(id)` | Get job by ID |
|
|
36
|
+
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
41
|
+
│ SchedulerService │
|
|
42
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
43
|
+
│ State: │
|
|
44
|
+
│ ├── running: boolean │
|
|
45
|
+
│ ├── timer: setTimeout handle │
|
|
46
|
+
│ └── op: Promise chain (for locking) │
|
|
47
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
48
|
+
│ Core Flow: │
|
|
49
|
+
│ 1. armTimer() → Set precise timer to next wake time │
|
|
50
|
+
│ 2. onTimer() → Find due jobs, mark running, execute │
|
|
51
|
+
│ 3. executeJob() → Run payload, deliver, apply result │
|
|
52
|
+
│ 4. applyResult() → Update state, compute next run │
|
|
53
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
### Precise Timer
|
|
59
|
+
|
|
60
|
+
Unlike polling-based schedulers, this implementation arms the timer to the exact next wake time:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
private armTimer(): void {
|
|
64
|
+
const nextAt = this.nextWakeAtMs() // Find earliest job
|
|
65
|
+
if (!nextAt) return
|
|
66
|
+
|
|
67
|
+
const delay = Math.max(0, nextAt - Date.now())
|
|
68
|
+
this.state.timer = setTimeout(() => this.onTimer(), delay)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Concurrency Protection
|
|
73
|
+
|
|
74
|
+
Uses `runningAtMs` marker to prevent double-execution:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
if (typeof job.state.runningAtMs === "number") {
|
|
78
|
+
// Already running - check if stuck (> 2 hours)
|
|
79
|
+
if (Date.now() - job.state.runningAtMs > STUCK_RUN_MS) {
|
|
80
|
+
return true // Stuck, allow re-run
|
|
81
|
+
}
|
|
82
|
+
return false // Still running
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Error Backoff
|
|
87
|
+
|
|
88
|
+
Exponential backoff on consecutive errors:
|
|
89
|
+
|
|
90
|
+
| Errors | Backoff |
|
|
91
|
+
|--------|---------|
|
|
92
|
+
| 1 | 30 seconds |
|
|
93
|
+
| 2 | 60 seconds |
|
|
94
|
+
| 3 | 5 minutes |
|
|
95
|
+
| 4 | 15 minutes |
|
|
96
|
+
| 5+ | 1 hour |
|
|
97
|
+
|
|
98
|
+
### Missed Job Catchup
|
|
99
|
+
|
|
100
|
+
On startup, clears stale `runningAtMs` markers (from crashes) and runs any due jobs.
|
|
101
|
+
|
|
102
|
+
## Job Definition
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
interface CronJob {
|
|
106
|
+
id: string
|
|
107
|
+
agentId?: string
|
|
108
|
+
sessionKey?: string // For reminder delivery context
|
|
109
|
+
name: string
|
|
110
|
+
description?: string
|
|
111
|
+
enabled: boolean
|
|
112
|
+
deleteAfterRun?: boolean // Auto-delete after successful run
|
|
113
|
+
createdAtMs: number
|
|
114
|
+
updatedAtMs: number
|
|
115
|
+
schedule: CronSchedule
|
|
116
|
+
sessionTarget: "main" | "isolated"
|
|
117
|
+
wakeMode: "now" | "next-heartbeat"
|
|
118
|
+
payload: CronPayload
|
|
119
|
+
delivery?: CronDelivery
|
|
120
|
+
state: CronJobState
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Payload Types
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
type CronPayload =
|
|
128
|
+
| { kind: "systemEvent"; text: string }
|
|
129
|
+
| {
|
|
130
|
+
kind: "agentTurn"
|
|
131
|
+
message: string
|
|
132
|
+
model?: string
|
|
133
|
+
thinking?: string
|
|
134
|
+
timeoutSeconds?: number
|
|
135
|
+
allowUnsafeExternalContent?: boolean
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Delivery Configuration
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
interface CronDelivery {
|
|
143
|
+
mode: "none" | "announce"
|
|
144
|
+
channel?: string // "xmtp", etc.
|
|
145
|
+
to?: string // Recipient address
|
|
146
|
+
accountId?: string
|
|
147
|
+
bestEffort?: boolean
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Usage
|
|
152
|
+
|
|
153
|
+
### Basic Setup
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { SchedulerService, createSqliteStore } from "@hybrd/scheduler"
|
|
157
|
+
|
|
158
|
+
const store = await createSqliteStore({ dbPath: "./data/scheduler.db" })
|
|
159
|
+
|
|
160
|
+
const scheduler = new SchedulerService({
|
|
161
|
+
store,
|
|
162
|
+
dispatcher: channelDispatcher,
|
|
163
|
+
executor: {
|
|
164
|
+
runAgentTurn: async (job) => { /* ... */ },
|
|
165
|
+
runSystemEvent: async (job) => { /* ... */ }
|
|
166
|
+
},
|
|
167
|
+
enabled: true
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await scheduler.start()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Schedule a One-Time Task
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
await scheduler.add({
|
|
177
|
+
name: "Reminder",
|
|
178
|
+
schedule: { kind: "at", at: "2026-03-01T09:00:00Z" },
|
|
179
|
+
payload: { kind: "agentTurn", message: "Check on the project" },
|
|
180
|
+
delivery: { mode: "announce", channel: "xmtp", to: "0x..." }
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Schedule an Interval Task
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
await scheduler.add({
|
|
188
|
+
name: "Status Check",
|
|
189
|
+
schedule: { kind: "every", everyMs: 300000 }, // 5 minutes
|
|
190
|
+
payload: { kind: "agentTurn", message: "Check status" }
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Schedule a Cron Task
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
await scheduler.add({
|
|
198
|
+
name: "Daily Standup",
|
|
199
|
+
schedule: { kind: "cron", expr: "0 9 * * 1-5", tz: "America/Chicago" },
|
|
200
|
+
payload: { kind: "agentTurn", message: "Time for standup!" }
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## MCP Tools
|
|
205
|
+
|
|
206
|
+
The scheduler provides ready-to-use MCP tools for agent integration:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { createSchedulerTools } from "@hybrd/scheduler"
|
|
210
|
+
|
|
211
|
+
const tools = createSchedulerTools(scheduler)
|
|
212
|
+
|
|
213
|
+
// Use with Claude Agent SDK
|
|
214
|
+
const agent = new Agent({
|
|
215
|
+
tools: [...otherTools, ...tools]
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Available Tools
|
|
220
|
+
|
|
221
|
+
| Tool | Description |
|
|
222
|
+
|------|-------------|
|
|
223
|
+
| `schedule_task` | Create a new scheduled task |
|
|
224
|
+
| `list_tasks` | List tasks with pagination and filtering |
|
|
225
|
+
| `cancel_task` | Cancel and remove a task |
|
|
226
|
+
| `get_task` | Get task details by ID |
|
|
227
|
+
| `run_task` | Manually trigger a task |
|
|
228
|
+
|
|
229
|
+
## Channel Adapters
|
|
230
|
+
|
|
231
|
+
Scheduler jobs can deliver results via channel adapters:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface ChannelAdapter {
|
|
235
|
+
readonly channel: ChannelId
|
|
236
|
+
readonly port: number
|
|
237
|
+
start(): Promise<void>
|
|
238
|
+
stop(): Promise<void>
|
|
239
|
+
trigger(req: TriggerRequest): Promise<TriggerResponse>
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Built-in Adapters
|
|
244
|
+
|
|
245
|
+
| Channel | Port | Description |
|
|
246
|
+
|---------|------|-------------|
|
|
247
|
+
| `xmtp` | 8455 | XMTP messaging |
|
|
248
|
+
|
|
249
|
+
### Custom Adapter
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
class SlackAdapter implements ChannelAdapter {
|
|
253
|
+
readonly channel = "slack"
|
|
254
|
+
readonly port = 8457
|
|
255
|
+
|
|
256
|
+
async trigger(req: TriggerRequest): Promise<TriggerResponse> {
|
|
257
|
+
// POST to Slack API
|
|
258
|
+
return { delivered: true, messageId: "..." }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Feature Comparison
|
|
264
|
+
|
|
265
|
+
| Feature | OpenClaw | Hybrid |
|
|
266
|
+
|---------|:--------:|:------:|
|
|
267
|
+
| Precise timer | ✅ | ✅ |
|
|
268
|
+
| Concurrency protection | ✅ | ✅ |
|
|
269
|
+
| Missed job catchup | ✅ | ✅ |
|
|
270
|
+
| Error backoff | ✅ | ✅ |
|
|
271
|
+
| Pagination API | ✅ | ✅ |
|
|
272
|
+
| SQLite storage | ❌ | ✅ |
|
|
273
|
+
| MCP tools | ❌ | ✅ |
|
|
274
|
+
| Channel adapters | ❌ | ✅ |
|
|
275
|
+
| XMTP integration | ❌ | ✅ |
|
|
276
|
+
|
|
277
|
+
## Storage
|
|
278
|
+
|
|
279
|
+
Uses SQLite via sql.js (WASM) for persistence:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
interface ScheduledTaskRow {
|
|
283
|
+
id: string
|
|
284
|
+
name: string
|
|
285
|
+
schedule: string // JSON
|
|
286
|
+
payload: string // JSON
|
|
287
|
+
delivery: string // JSON
|
|
288
|
+
state: string // JSON
|
|
289
|
+
enabled: number // 0 or 1
|
|
290
|
+
created_at: number
|
|
291
|
+
updated_at: number
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Benefits over JSON file storage:
|
|
296
|
+
- Atomic writes
|
|
297
|
+
- Query support
|
|
298
|
+
- Better performance
|
|
299
|
+
- Works in WASM environments
|
|
300
|
+
|
|
301
|
+
## Configuration
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
interface SchedulerConfig {
|
|
305
|
+
store: SqliteSchedulerStore
|
|
306
|
+
dispatcher: ChannelDispatcher
|
|
307
|
+
executor: SchedulerExecutor
|
|
308
|
+
enabled?: boolean // Default: true
|
|
309
|
+
timezone?: string // Default: system timezone
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Environment Variables
|
|
314
|
+
|
|
315
|
+
| Variable | Default | Description |
|
|
316
|
+
|----------|---------|-------------|
|
|
317
|
+
| `SCHEDULER_ENABLED` | `true` | Enable/disable scheduler |
|
|
318
|
+
| `SCHEDULER_DB_PATH` | `./data/scheduler.db` | SQLite database path |
|
|
319
|
+
| `SCHEDULER_TIMEZONE` | System TZ | Default timezone for cron |
|
|
320
|
+
|
|
321
|
+
## Events
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
scheduler.onEvent((event) => {
|
|
325
|
+
// event.action: "added" | "updated" | "removed" | "started" | "finished"
|
|
326
|
+
console.log(`Job ${event.jobId}: ${event.action}`)
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Testing
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
cd packages/scheduler
|
|
334
|
+
pnpm test
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
MIT
|