digital-tasks 2.0.1

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/task.ts ADDED
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Task - Core task creation and management functions
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ import type {
8
+ Task,
9
+ AnyTask,
10
+ CreateTaskOptions,
11
+ WorkerRef,
12
+ TaskResult,
13
+ TaskDependency,
14
+ } from './types.js'
15
+ import { taskQueue } from './queue.js'
16
+
17
+ /**
18
+ * Generate a unique task ID
19
+ */
20
+ function generateTaskId(): string {
21
+ return `task_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
22
+ }
23
+
24
+ /**
25
+ * Create a new task from a function definition
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const task = await createTask({
30
+ * function: {
31
+ * type: 'generative',
32
+ * name: 'summarize',
33
+ * args: { text: 'The text to summarize' },
34
+ * output: 'string',
35
+ * promptTemplate: 'Summarize: {{text}}',
36
+ * },
37
+ * input: { text: 'Long article content...' },
38
+ * priority: 'high',
39
+ * })
40
+ * ```
41
+ */
42
+ export async function createTask<TInput = unknown, TOutput = unknown>(
43
+ options: CreateTaskOptions<TInput, TOutput>
44
+ ): Promise<Task<TInput, TOutput>> {
45
+ const now = new Date()
46
+
47
+ // Convert string dependencies to TaskDependency array
48
+ let dependencies: TaskDependency[] | undefined
49
+ if (options.dependencies && options.dependencies.length > 0) {
50
+ dependencies = options.dependencies.map((taskId) => ({
51
+ type: 'blocked_by' as const,
52
+ taskId,
53
+ satisfied: false,
54
+ }))
55
+ }
56
+
57
+ // Determine allowed workers from function type
58
+ let allowedWorkers = options.allowedWorkers
59
+ if (!allowedWorkers) {
60
+ const funcType = options.function.type
61
+ if (funcType === 'human') {
62
+ allowedWorkers = ['human']
63
+ } else if (funcType === 'agentic') {
64
+ allowedWorkers = ['agent']
65
+ } else {
66
+ allowedWorkers = ['any']
67
+ }
68
+ }
69
+
70
+ const task: Task<TInput, TOutput> = {
71
+ id: generateTaskId(),
72
+ function: options.function,
73
+ status: options.scheduledFor ? 'pending' : 'queued',
74
+ priority: options.priority || 'normal',
75
+ input: options.input,
76
+ allowedWorkers,
77
+ dependencies,
78
+ scheduledFor: options.scheduledFor,
79
+ deadline: options.deadline,
80
+ timeout: options.timeout,
81
+ tags: options.tags,
82
+ parentId: options.parentId,
83
+ projectId: options.projectId,
84
+ metadata: options.metadata,
85
+ createdAt: now,
86
+ events: [],
87
+ }
88
+
89
+ // Auto-assign if specified
90
+ if (options.assignTo) {
91
+ task.assignment = {
92
+ worker: options.assignTo,
93
+ assignedAt: now,
94
+ }
95
+ task.status = 'assigned'
96
+ }
97
+
98
+ // Check if blocked by dependencies
99
+ if (dependencies && dependencies.length > 0) {
100
+ const hasPendingDeps = dependencies.some(
101
+ (d) => d.type === 'blocked_by' && !d.satisfied
102
+ )
103
+ if (hasPendingDeps) {
104
+ task.status = 'blocked'
105
+ }
106
+ }
107
+
108
+ // Add to queue
109
+ await taskQueue.add(task as AnyTask)
110
+
111
+ return task
112
+ }
113
+
114
+ /**
115
+ * Get a task by ID
116
+ */
117
+ export async function getTask(id: string): Promise<AnyTask | undefined> {
118
+ return taskQueue.get(id)
119
+ }
120
+
121
+ /**
122
+ * Start working on a task
123
+ */
124
+ export async function startTask(
125
+ taskId: string,
126
+ worker: WorkerRef
127
+ ): Promise<AnyTask | undefined> {
128
+ const task = await taskQueue.get(taskId)
129
+ if (!task) return undefined
130
+
131
+ // Claim the task if not already assigned
132
+ if (!task.assignment) {
133
+ const claimed = await taskQueue.claim(taskId, worker)
134
+ if (!claimed) return undefined
135
+ }
136
+
137
+ // Update status to in_progress
138
+ return taskQueue.update(taskId, {
139
+ status: 'in_progress',
140
+ event: {
141
+ type: 'started',
142
+ actor: worker,
143
+ message: `Started by ${worker.name || worker.id}`,
144
+ },
145
+ })
146
+ }
147
+
148
+ /**
149
+ * Update task progress
150
+ */
151
+ export async function updateProgress(
152
+ taskId: string,
153
+ percent: number,
154
+ step?: string
155
+ ): Promise<AnyTask | undefined> {
156
+ return taskQueue.update(taskId, {
157
+ progress: {
158
+ percent,
159
+ step,
160
+ updatedAt: new Date(),
161
+ },
162
+ event: {
163
+ type: 'progress',
164
+ message: step || `Progress: ${percent}%`,
165
+ data: { percent, step },
166
+ },
167
+ })
168
+ }
169
+
170
+ /**
171
+ * Complete a task with output
172
+ */
173
+ export async function completeTask<TOutput>(
174
+ taskId: string,
175
+ output: TOutput
176
+ ): Promise<TaskResult<TOutput>> {
177
+ const task = await taskQueue.get(taskId)
178
+ if (!task) {
179
+ return {
180
+ taskId,
181
+ success: false,
182
+ error: {
183
+ code: 'TASK_NOT_FOUND',
184
+ message: `Task "${taskId}" not found`,
185
+ },
186
+ }
187
+ }
188
+
189
+ await taskQueue.complete(taskId, output)
190
+
191
+ return {
192
+ taskId,
193
+ success: true,
194
+ output,
195
+ metadata: {
196
+ duration: task.startedAt
197
+ ? Date.now() - task.startedAt.getTime()
198
+ : 0,
199
+ startedAt: task.startedAt || new Date(),
200
+ completedAt: new Date(),
201
+ worker: task.assignment?.worker,
202
+ },
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Fail a task with error
208
+ */
209
+ export async function failTask(
210
+ taskId: string,
211
+ error: string | Error
212
+ ): Promise<TaskResult> {
213
+ const task = await taskQueue.get(taskId)
214
+ if (!task) {
215
+ return {
216
+ taskId,
217
+ success: false,
218
+ error: {
219
+ code: 'TASK_NOT_FOUND',
220
+ message: `Task "${taskId}" not found`,
221
+ },
222
+ }
223
+ }
224
+
225
+ const errorMessage = error instanceof Error ? error.message : error
226
+
227
+ await taskQueue.fail(taskId, errorMessage)
228
+
229
+ return {
230
+ taskId,
231
+ success: false,
232
+ error: {
233
+ code: 'TASK_FAILED',
234
+ message: errorMessage,
235
+ details: error instanceof Error ? { stack: error.stack } : undefined,
236
+ },
237
+ metadata: {
238
+ duration: task.startedAt
239
+ ? Date.now() - task.startedAt.getTime()
240
+ : 0,
241
+ startedAt: task.startedAt || new Date(),
242
+ completedAt: new Date(),
243
+ worker: task.assignment?.worker,
244
+ },
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Cancel a task
250
+ */
251
+ export async function cancelTask(
252
+ taskId: string,
253
+ reason?: string
254
+ ): Promise<boolean> {
255
+ const task = await taskQueue.get(taskId)
256
+ if (!task) return false
257
+
258
+ await taskQueue.update(taskId, {
259
+ status: 'cancelled',
260
+ event: {
261
+ type: 'cancelled',
262
+ message: reason || 'Task cancelled',
263
+ },
264
+ })
265
+
266
+ return true
267
+ }
268
+
269
+ /**
270
+ * Add a comment to a task
271
+ */
272
+ export async function addComment(
273
+ taskId: string,
274
+ comment: string,
275
+ author?: WorkerRef
276
+ ): Promise<AnyTask | undefined> {
277
+ return taskQueue.update(taskId, {
278
+ event: {
279
+ type: 'comment',
280
+ actor: author,
281
+ message: comment,
282
+ },
283
+ })
284
+ }
285
+
286
+ /**
287
+ * Create a subtask
288
+ */
289
+ export async function createSubtask<TInput = unknown, TOutput = unknown>(
290
+ parentTaskId: string,
291
+ options: Omit<CreateTaskOptions<TInput, TOutput>, 'parentId'>
292
+ ): Promise<Task<TInput, TOutput>> {
293
+ return createTask({
294
+ ...options,
295
+ parentId: parentTaskId,
296
+ })
297
+ }
298
+
299
+ /**
300
+ * Get subtasks of a task
301
+ */
302
+ export async function getSubtasks(parentTaskId: string): Promise<AnyTask[]> {
303
+ return taskQueue.query({ parentId: parentTaskId })
304
+ }
305
+
306
+ /**
307
+ * Wait for a task to complete
308
+ */
309
+ export async function waitForTask(
310
+ taskId: string,
311
+ options?: { timeout?: number; pollInterval?: number }
312
+ ): Promise<TaskResult> {
313
+ const timeout = options?.timeout ?? 5 * 60 * 1000 // 5 minutes
314
+ const pollInterval = options?.pollInterval ?? 1000 // 1 second
315
+
316
+ const startTime = Date.now()
317
+
318
+ while (Date.now() - startTime < timeout) {
319
+ const task = await taskQueue.get(taskId)
320
+ if (!task) {
321
+ return {
322
+ taskId,
323
+ success: false,
324
+ error: {
325
+ code: 'TASK_NOT_FOUND',
326
+ message: `Task "${taskId}" not found`,
327
+ },
328
+ }
329
+ }
330
+
331
+ if (task.status === 'completed') {
332
+ return {
333
+ taskId,
334
+ success: true,
335
+ output: task.output,
336
+ metadata: {
337
+ duration: task.completedAt && task.startedAt
338
+ ? task.completedAt.getTime() - task.startedAt.getTime()
339
+ : 0,
340
+ startedAt: task.startedAt || task.createdAt,
341
+ completedAt: task.completedAt || new Date(),
342
+ worker: task.assignment?.worker,
343
+ },
344
+ }
345
+ }
346
+
347
+ if (task.status === 'failed') {
348
+ return {
349
+ taskId,
350
+ success: false,
351
+ error: {
352
+ code: 'TASK_FAILED',
353
+ message: task.error || 'Task failed',
354
+ },
355
+ metadata: {
356
+ duration: task.completedAt && task.startedAt
357
+ ? task.completedAt.getTime() - task.startedAt.getTime()
358
+ : 0,
359
+ startedAt: task.startedAt || task.createdAt,
360
+ completedAt: task.completedAt || new Date(),
361
+ worker: task.assignment?.worker,
362
+ },
363
+ }
364
+ }
365
+
366
+ if (task.status === 'cancelled') {
367
+ return {
368
+ taskId,
369
+ success: false,
370
+ error: {
371
+ code: 'TASK_CANCELLED',
372
+ message: 'Task was cancelled',
373
+ },
374
+ }
375
+ }
376
+
377
+ // Wait before polling again
378
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
379
+ }
380
+
381
+ return {
382
+ taskId,
383
+ success: false,
384
+ error: {
385
+ code: 'TIMEOUT',
386
+ message: `Task did not complete within ${timeout}ms`,
387
+ },
388
+ }
389
+ }