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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +10 -0
- package/dist/function-task.d.ts +319 -0
- package/dist/function-task.d.ts.map +1 -0
- package/dist/function-task.js +286 -0
- package/dist/function-task.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +112 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +510 -0
- package/dist/markdown.js.map +1 -0
- package/dist/project.d.ts +259 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +397 -0
- package/dist/project.js.map +1 -0
- package/dist/queue.d.ts +17 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +347 -0
- package/dist/queue.js.map +1 -0
- package/dist/task.d.ts +69 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +321 -0
- package/dist/task.js.map +1 -0
- package/dist/types.d.ts +292 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +161 -0
- package/src/markdown.ts +622 -0
- package/src/project.ts +571 -0
- package/src/queue.ts +424 -0
- package/src/task.ts +389 -0
- package/src/types.ts +403 -0
- package/test/markdown.test.ts +550 -0
- package/test/project.test.ts +562 -0
- package/test/queue.test.ts +482 -0
- package/test/task.test.ts +464 -0
- package/tsconfig.json +20 -0
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
|
+
}
|