digital-tasks 2.0.2 → 2.1.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/CHANGELOG.md +19 -0
- package/package.json +4 -5
- package/src/index.js +73 -0
- package/src/markdown.js +509 -0
- package/src/project.js +396 -0
- package/src/queue.js +346 -0
- package/src/task.js +320 -0
- package/src/types.js +14 -0
- package/test/markdown.test.js +451 -0
- package/test/project.test.js +427 -0
- package/test/queue.test.js +407 -0
- package/test/task.test.js +370 -0
package/src/queue.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Queue - In-memory task queue implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides task queuing, assignment, and execution management.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Priority values for sorting
|
|
10
|
+
*/
|
|
11
|
+
const priorityOrder = {
|
|
12
|
+
critical: 5,
|
|
13
|
+
urgent: 4,
|
|
14
|
+
high: 3,
|
|
15
|
+
normal: 2,
|
|
16
|
+
low: 1,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* In-memory task queue implementation
|
|
20
|
+
*/
|
|
21
|
+
class InMemoryTaskQueue {
|
|
22
|
+
tasks = new Map();
|
|
23
|
+
options;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.options = {
|
|
26
|
+
name: 'default',
|
|
27
|
+
concurrency: 10,
|
|
28
|
+
defaultTimeout: 5 * 60 * 1000, // 5 minutes
|
|
29
|
+
persistent: false,
|
|
30
|
+
...options,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async add(task) {
|
|
34
|
+
// Add created event
|
|
35
|
+
const event = {
|
|
36
|
+
id: `evt_${Date.now()}`,
|
|
37
|
+
type: 'created',
|
|
38
|
+
timestamp: new Date(),
|
|
39
|
+
message: `Task created: ${task.function.name}`,
|
|
40
|
+
};
|
|
41
|
+
const taskWithEvent = {
|
|
42
|
+
...task,
|
|
43
|
+
events: [...(task.events || []), event],
|
|
44
|
+
};
|
|
45
|
+
this.tasks.set(task.id, taskWithEvent);
|
|
46
|
+
}
|
|
47
|
+
async get(id) {
|
|
48
|
+
return this.tasks.get(id);
|
|
49
|
+
}
|
|
50
|
+
async update(id, options) {
|
|
51
|
+
const task = this.tasks.get(id);
|
|
52
|
+
if (!task)
|
|
53
|
+
return undefined;
|
|
54
|
+
const events = [...(task.events || [])];
|
|
55
|
+
// Add event if provided
|
|
56
|
+
if (options.event) {
|
|
57
|
+
events.push({
|
|
58
|
+
...options.event,
|
|
59
|
+
id: `evt_${Date.now()}`,
|
|
60
|
+
timestamp: new Date(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Add status change event
|
|
64
|
+
if (options.status && options.status !== task.status) {
|
|
65
|
+
events.push({
|
|
66
|
+
id: `evt_${Date.now()}_status`,
|
|
67
|
+
type: options.status === 'completed' ? 'completed' :
|
|
68
|
+
options.status === 'failed' ? 'failed' :
|
|
69
|
+
options.status === 'in_progress' ? 'started' :
|
|
70
|
+
options.status === 'blocked' ? 'blocked' : 'progress',
|
|
71
|
+
timestamp: new Date(),
|
|
72
|
+
message: `Status changed from ${task.status} to ${options.status}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Build progress update if provided
|
|
76
|
+
let progressUpdate = task.progress;
|
|
77
|
+
if (options.progress) {
|
|
78
|
+
progressUpdate = {
|
|
79
|
+
percent: options.progress.percent ?? task.progress?.percent ?? 0,
|
|
80
|
+
step: options.progress.step ?? task.progress?.step,
|
|
81
|
+
totalSteps: options.progress.totalSteps ?? task.progress?.totalSteps,
|
|
82
|
+
currentStep: options.progress.currentStep ?? task.progress?.currentStep,
|
|
83
|
+
estimatedTimeRemaining: options.progress.estimatedTimeRemaining ?? task.progress?.estimatedTimeRemaining,
|
|
84
|
+
updatedAt: new Date(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const updated = {
|
|
88
|
+
...task,
|
|
89
|
+
...(options.status && { status: options.status }),
|
|
90
|
+
...(options.priority && { priority: options.priority }),
|
|
91
|
+
...(options.assignment && { assignment: options.assignment }),
|
|
92
|
+
...(progressUpdate && { progress: progressUpdate }),
|
|
93
|
+
...(options.metadata && {
|
|
94
|
+
metadata: { ...task.metadata, ...options.metadata },
|
|
95
|
+
}),
|
|
96
|
+
events,
|
|
97
|
+
};
|
|
98
|
+
this.tasks.set(id, updated);
|
|
99
|
+
return updated;
|
|
100
|
+
}
|
|
101
|
+
async remove(id) {
|
|
102
|
+
return this.tasks.delete(id);
|
|
103
|
+
}
|
|
104
|
+
async query(options) {
|
|
105
|
+
let results = Array.from(this.tasks.values());
|
|
106
|
+
// Filter by status
|
|
107
|
+
if (options.status) {
|
|
108
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
109
|
+
results = results.filter((t) => statuses.includes(t.status));
|
|
110
|
+
}
|
|
111
|
+
// Filter by priority
|
|
112
|
+
if (options.priority) {
|
|
113
|
+
const priorities = Array.isArray(options.priority) ? options.priority : [options.priority];
|
|
114
|
+
results = results.filter((t) => priorities.includes(t.priority));
|
|
115
|
+
}
|
|
116
|
+
// Filter by function type
|
|
117
|
+
if (options.functionType) {
|
|
118
|
+
results = results.filter((t) => t.function.type === options.functionType);
|
|
119
|
+
}
|
|
120
|
+
// Filter by assigned worker
|
|
121
|
+
if (options.assignedTo) {
|
|
122
|
+
results = results.filter((t) => t.assignment?.worker.id === options.assignedTo);
|
|
123
|
+
}
|
|
124
|
+
// Filter by tags
|
|
125
|
+
if (options.tags && options.tags.length > 0) {
|
|
126
|
+
results = results.filter((t) => t.tags && options.tags.some((tag) => t.tags.includes(tag)));
|
|
127
|
+
}
|
|
128
|
+
// Filter by project
|
|
129
|
+
if (options.projectId) {
|
|
130
|
+
results = results.filter((t) => t.projectId === options.projectId);
|
|
131
|
+
}
|
|
132
|
+
// Filter by parent
|
|
133
|
+
if (options.parentId) {
|
|
134
|
+
results = results.filter((t) => t.parentId === options.parentId);
|
|
135
|
+
}
|
|
136
|
+
// Text search
|
|
137
|
+
if (options.search) {
|
|
138
|
+
const search = options.search.toLowerCase();
|
|
139
|
+
results = results.filter((t) => t.function.name.toLowerCase().includes(search) ||
|
|
140
|
+
t.function.description?.toLowerCase().includes(search));
|
|
141
|
+
}
|
|
142
|
+
// Sort
|
|
143
|
+
if (options.sortBy) {
|
|
144
|
+
results.sort((a, b) => {
|
|
145
|
+
let aVal;
|
|
146
|
+
let bVal;
|
|
147
|
+
switch (options.sortBy) {
|
|
148
|
+
case 'createdAt':
|
|
149
|
+
aVal = a.createdAt.getTime();
|
|
150
|
+
bVal = b.createdAt.getTime();
|
|
151
|
+
break;
|
|
152
|
+
case 'priority':
|
|
153
|
+
aVal = priorityOrder[a.priority];
|
|
154
|
+
bVal = priorityOrder[b.priority];
|
|
155
|
+
break;
|
|
156
|
+
case 'deadline':
|
|
157
|
+
aVal = a.deadline?.getTime() || Infinity;
|
|
158
|
+
bVal = b.deadline?.getTime() || Infinity;
|
|
159
|
+
break;
|
|
160
|
+
case 'status':
|
|
161
|
+
aVal = a.status.charCodeAt(0);
|
|
162
|
+
bVal = b.status.charCodeAt(0);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
return options.sortOrder === 'desc' ? bVal - aVal : aVal - bVal;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Pagination
|
|
171
|
+
const offset = options.offset ?? 0;
|
|
172
|
+
const limit = options.limit ?? results.length;
|
|
173
|
+
results = results.slice(offset, offset + limit);
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
async getNextForWorker(worker) {
|
|
177
|
+
// Get queued tasks sorted by priority, then by deadline
|
|
178
|
+
const queuedTasks = await this.query({
|
|
179
|
+
status: ['pending', 'queued'],
|
|
180
|
+
sortBy: 'priority',
|
|
181
|
+
sortOrder: 'desc',
|
|
182
|
+
});
|
|
183
|
+
// Find first task the worker can handle
|
|
184
|
+
for (const task of queuedTasks) {
|
|
185
|
+
// Check if worker type is allowed
|
|
186
|
+
if (task.allowedWorkers && !task.allowedWorkers.includes(worker.type) && !task.allowedWorkers.includes('any')) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Note: Could check worker skills against task requirements in the future
|
|
190
|
+
// Check if task is scheduled for later
|
|
191
|
+
if (task.scheduledFor && task.scheduledFor > new Date()) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// Check dependencies
|
|
195
|
+
if (task.dependencies && task.dependencies.length > 0) {
|
|
196
|
+
const unblockedDeps = task.dependencies.filter((d) => d.type === 'blocked_by' && !d.satisfied);
|
|
197
|
+
if (unblockedDeps.length > 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return task;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
async claim(taskId, worker) {
|
|
206
|
+
const task = await this.get(taskId);
|
|
207
|
+
if (!task)
|
|
208
|
+
return false;
|
|
209
|
+
// Check if already assigned
|
|
210
|
+
if (task.assignment) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
// Update task with assignment
|
|
214
|
+
await this.update(taskId, {
|
|
215
|
+
status: 'assigned',
|
|
216
|
+
assignment: {
|
|
217
|
+
worker,
|
|
218
|
+
assignedAt: new Date(),
|
|
219
|
+
},
|
|
220
|
+
event: {
|
|
221
|
+
type: 'assigned',
|
|
222
|
+
actor: worker,
|
|
223
|
+
message: `Assigned to ${worker.name || worker.id}`,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
async complete(taskId, output) {
|
|
229
|
+
const task = await this.get(taskId);
|
|
230
|
+
if (!task)
|
|
231
|
+
return;
|
|
232
|
+
await this.update(taskId, {
|
|
233
|
+
status: 'completed',
|
|
234
|
+
event: {
|
|
235
|
+
type: 'completed',
|
|
236
|
+
actor: task.assignment?.worker,
|
|
237
|
+
message: 'Task completed successfully',
|
|
238
|
+
data: { output },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
// Update task output separately since it's not in UpdateTaskOptions
|
|
242
|
+
const updated = await this.get(taskId);
|
|
243
|
+
if (updated) {
|
|
244
|
+
this.tasks.set(taskId, {
|
|
245
|
+
...updated,
|
|
246
|
+
output: {
|
|
247
|
+
value: output,
|
|
248
|
+
producedAt: new Date(),
|
|
249
|
+
},
|
|
250
|
+
completedAt: new Date(),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// Satisfy dependencies in other tasks
|
|
254
|
+
for (const [, otherTask] of this.tasks) {
|
|
255
|
+
if (otherTask.dependencies) {
|
|
256
|
+
const hasDep = otherTask.dependencies.find((d) => d.taskId === taskId && d.type === 'blocked_by');
|
|
257
|
+
if (hasDep) {
|
|
258
|
+
const updatedDeps = otherTask.dependencies.map((d) => d.taskId === taskId ? { ...d, satisfied: true } : d);
|
|
259
|
+
this.tasks.set(otherTask.id, {
|
|
260
|
+
...otherTask,
|
|
261
|
+
dependencies: updatedDeps,
|
|
262
|
+
});
|
|
263
|
+
// Unblock if all dependencies satisfied
|
|
264
|
+
const allSatisfied = updatedDeps.filter((d) => d.type === 'blocked_by').every((d) => d.satisfied);
|
|
265
|
+
if (allSatisfied && otherTask.status === 'blocked') {
|
|
266
|
+
await this.update(otherTask.id, {
|
|
267
|
+
status: 'queued',
|
|
268
|
+
event: {
|
|
269
|
+
type: 'unblocked',
|
|
270
|
+
message: 'All dependencies satisfied',
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async fail(taskId, error) {
|
|
279
|
+
const task = await this.get(taskId);
|
|
280
|
+
if (!task)
|
|
281
|
+
return;
|
|
282
|
+
await this.update(taskId, {
|
|
283
|
+
status: 'failed',
|
|
284
|
+
event: {
|
|
285
|
+
type: 'failed',
|
|
286
|
+
actor: task.assignment?.worker,
|
|
287
|
+
message: `Task failed: ${error}`,
|
|
288
|
+
data: { error },
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async stats() {
|
|
293
|
+
const tasks = Array.from(this.tasks.values());
|
|
294
|
+
const byStatus = {
|
|
295
|
+
pending: 0,
|
|
296
|
+
queued: 0,
|
|
297
|
+
assigned: 0,
|
|
298
|
+
in_progress: 0,
|
|
299
|
+
blocked: 0,
|
|
300
|
+
review: 0,
|
|
301
|
+
completed: 0,
|
|
302
|
+
failed: 0,
|
|
303
|
+
cancelled: 0,
|
|
304
|
+
};
|
|
305
|
+
const byPriority = {
|
|
306
|
+
low: 0,
|
|
307
|
+
normal: 0,
|
|
308
|
+
high: 0,
|
|
309
|
+
urgent: 0,
|
|
310
|
+
critical: 0,
|
|
311
|
+
};
|
|
312
|
+
let totalWaitTime = 0;
|
|
313
|
+
let waitTimeCount = 0;
|
|
314
|
+
let totalCompletionTime = 0;
|
|
315
|
+
let completionTimeCount = 0;
|
|
316
|
+
for (const task of tasks) {
|
|
317
|
+
byStatus[task.status]++;
|
|
318
|
+
byPriority[task.priority]++;
|
|
319
|
+
if (task.startedAt && task.createdAt) {
|
|
320
|
+
totalWaitTime += task.startedAt.getTime() - task.createdAt.getTime();
|
|
321
|
+
waitTimeCount++;
|
|
322
|
+
}
|
|
323
|
+
if (task.completedAt && task.startedAt) {
|
|
324
|
+
totalCompletionTime += task.completedAt.getTime() - task.startedAt.getTime();
|
|
325
|
+
completionTimeCount++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
total: tasks.length,
|
|
330
|
+
byStatus,
|
|
331
|
+
byPriority,
|
|
332
|
+
avgWaitTime: waitTimeCount > 0 ? totalWaitTime / waitTimeCount : undefined,
|
|
333
|
+
avgCompletionTime: completionTimeCount > 0 ? totalCompletionTime / completionTimeCount : undefined,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Global task queue instance
|
|
339
|
+
*/
|
|
340
|
+
export const taskQueue = new InMemoryTaskQueue();
|
|
341
|
+
/**
|
|
342
|
+
* Create a new task queue instance
|
|
343
|
+
*/
|
|
344
|
+
export function createTaskQueue(options) {
|
|
345
|
+
return new InMemoryTaskQueue(options);
|
|
346
|
+
}
|
package/src/task.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task - Core task creation and management functions
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { taskQueue } from './queue.js';
|
|
7
|
+
/**
|
|
8
|
+
* Generate a unique task ID
|
|
9
|
+
*/
|
|
10
|
+
function generateTaskId() {
|
|
11
|
+
return `task_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a new task from a function definition
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const task = await createTask({
|
|
19
|
+
* function: {
|
|
20
|
+
* type: 'generative',
|
|
21
|
+
* name: 'summarize',
|
|
22
|
+
* args: { text: 'The text to summarize' },
|
|
23
|
+
* output: 'string',
|
|
24
|
+
* promptTemplate: 'Summarize: {{text}}',
|
|
25
|
+
* },
|
|
26
|
+
* input: { text: 'Long article content...' },
|
|
27
|
+
* priority: 'high',
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export async function createTask(options) {
|
|
32
|
+
const now = new Date();
|
|
33
|
+
// Convert string dependencies to TaskDependency array
|
|
34
|
+
let dependencies;
|
|
35
|
+
if (options.dependencies && options.dependencies.length > 0) {
|
|
36
|
+
dependencies = options.dependencies.map((taskId) => ({
|
|
37
|
+
type: 'blocked_by',
|
|
38
|
+
taskId,
|
|
39
|
+
satisfied: false,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
// Determine allowed workers from function type
|
|
43
|
+
let allowedWorkers = options.allowedWorkers;
|
|
44
|
+
if (!allowedWorkers) {
|
|
45
|
+
const funcType = options.function.type;
|
|
46
|
+
if (funcType === 'human') {
|
|
47
|
+
allowedWorkers = ['human'];
|
|
48
|
+
}
|
|
49
|
+
else if (funcType === 'agentic') {
|
|
50
|
+
allowedWorkers = ['agent'];
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
allowedWorkers = ['any'];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const task = {
|
|
57
|
+
id: generateTaskId(),
|
|
58
|
+
function: options.function,
|
|
59
|
+
status: options.scheduledFor ? 'pending' : 'queued',
|
|
60
|
+
priority: options.priority || 'normal',
|
|
61
|
+
input: options.input,
|
|
62
|
+
allowedWorkers,
|
|
63
|
+
dependencies,
|
|
64
|
+
scheduledFor: options.scheduledFor,
|
|
65
|
+
deadline: options.deadline,
|
|
66
|
+
timeout: options.timeout,
|
|
67
|
+
tags: options.tags,
|
|
68
|
+
parentId: options.parentId,
|
|
69
|
+
projectId: options.projectId,
|
|
70
|
+
metadata: options.metadata,
|
|
71
|
+
createdAt: now,
|
|
72
|
+
events: [],
|
|
73
|
+
};
|
|
74
|
+
// Auto-assign if specified
|
|
75
|
+
if (options.assignTo) {
|
|
76
|
+
task.assignment = {
|
|
77
|
+
worker: options.assignTo,
|
|
78
|
+
assignedAt: now,
|
|
79
|
+
};
|
|
80
|
+
task.status = 'assigned';
|
|
81
|
+
}
|
|
82
|
+
// Check if blocked by dependencies
|
|
83
|
+
if (dependencies && dependencies.length > 0) {
|
|
84
|
+
const hasPendingDeps = dependencies.some((d) => d.type === 'blocked_by' && !d.satisfied);
|
|
85
|
+
if (hasPendingDeps) {
|
|
86
|
+
task.status = 'blocked';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Add to queue
|
|
90
|
+
await taskQueue.add(task);
|
|
91
|
+
return task;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get a task by ID
|
|
95
|
+
*/
|
|
96
|
+
export async function getTask(id) {
|
|
97
|
+
return taskQueue.get(id);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Start working on a task
|
|
101
|
+
*/
|
|
102
|
+
export async function startTask(taskId, worker) {
|
|
103
|
+
const task = await taskQueue.get(taskId);
|
|
104
|
+
if (!task)
|
|
105
|
+
return undefined;
|
|
106
|
+
// Claim the task if not already assigned
|
|
107
|
+
if (!task.assignment) {
|
|
108
|
+
const claimed = await taskQueue.claim(taskId, worker);
|
|
109
|
+
if (!claimed)
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
// Update status to in_progress
|
|
113
|
+
return taskQueue.update(taskId, {
|
|
114
|
+
status: 'in_progress',
|
|
115
|
+
event: {
|
|
116
|
+
type: 'started',
|
|
117
|
+
actor: worker,
|
|
118
|
+
message: `Started by ${worker.name || worker.id}`,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Update task progress
|
|
124
|
+
*/
|
|
125
|
+
export async function updateProgress(taskId, percent, step) {
|
|
126
|
+
return taskQueue.update(taskId, {
|
|
127
|
+
progress: {
|
|
128
|
+
percent,
|
|
129
|
+
step,
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
},
|
|
132
|
+
event: {
|
|
133
|
+
type: 'progress',
|
|
134
|
+
message: step || `Progress: ${percent}%`,
|
|
135
|
+
data: { percent, step },
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Complete a task with output
|
|
141
|
+
*/
|
|
142
|
+
export async function completeTask(taskId, output) {
|
|
143
|
+
const task = await taskQueue.get(taskId);
|
|
144
|
+
if (!task) {
|
|
145
|
+
return {
|
|
146
|
+
taskId,
|
|
147
|
+
success: false,
|
|
148
|
+
error: {
|
|
149
|
+
code: 'TASK_NOT_FOUND',
|
|
150
|
+
message: `Task "${taskId}" not found`,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
await taskQueue.complete(taskId, output);
|
|
155
|
+
return {
|
|
156
|
+
taskId,
|
|
157
|
+
success: true,
|
|
158
|
+
output,
|
|
159
|
+
metadata: {
|
|
160
|
+
duration: task.startedAt
|
|
161
|
+
? Date.now() - task.startedAt.getTime()
|
|
162
|
+
: 0,
|
|
163
|
+
startedAt: task.startedAt || new Date(),
|
|
164
|
+
completedAt: new Date(),
|
|
165
|
+
worker: task.assignment?.worker,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Fail a task with error
|
|
171
|
+
*/
|
|
172
|
+
export async function failTask(taskId, error) {
|
|
173
|
+
const task = await taskQueue.get(taskId);
|
|
174
|
+
if (!task) {
|
|
175
|
+
return {
|
|
176
|
+
taskId,
|
|
177
|
+
success: false,
|
|
178
|
+
error: {
|
|
179
|
+
code: 'TASK_NOT_FOUND',
|
|
180
|
+
message: `Task "${taskId}" not found`,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
185
|
+
await taskQueue.fail(taskId, errorMessage);
|
|
186
|
+
return {
|
|
187
|
+
taskId,
|
|
188
|
+
success: false,
|
|
189
|
+
error: {
|
|
190
|
+
code: 'TASK_FAILED',
|
|
191
|
+
message: errorMessage,
|
|
192
|
+
details: error instanceof Error ? { stack: error.stack } : undefined,
|
|
193
|
+
},
|
|
194
|
+
metadata: {
|
|
195
|
+
duration: task.startedAt
|
|
196
|
+
? Date.now() - task.startedAt.getTime()
|
|
197
|
+
: 0,
|
|
198
|
+
startedAt: task.startedAt || new Date(),
|
|
199
|
+
completedAt: new Date(),
|
|
200
|
+
worker: task.assignment?.worker,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Cancel a task
|
|
206
|
+
*/
|
|
207
|
+
export async function cancelTask(taskId, reason) {
|
|
208
|
+
const task = await taskQueue.get(taskId);
|
|
209
|
+
if (!task)
|
|
210
|
+
return false;
|
|
211
|
+
await taskQueue.update(taskId, {
|
|
212
|
+
status: 'cancelled',
|
|
213
|
+
event: {
|
|
214
|
+
type: 'cancelled',
|
|
215
|
+
message: reason || 'Task cancelled',
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Add a comment to a task
|
|
222
|
+
*/
|
|
223
|
+
export async function addComment(taskId, comment, author) {
|
|
224
|
+
return taskQueue.update(taskId, {
|
|
225
|
+
event: {
|
|
226
|
+
type: 'comment',
|
|
227
|
+
actor: author,
|
|
228
|
+
message: comment,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Create a subtask
|
|
234
|
+
*/
|
|
235
|
+
export async function createSubtask(parentTaskId, options) {
|
|
236
|
+
return createTask({
|
|
237
|
+
...options,
|
|
238
|
+
parentId: parentTaskId,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get subtasks of a task
|
|
243
|
+
*/
|
|
244
|
+
export async function getSubtasks(parentTaskId) {
|
|
245
|
+
return taskQueue.query({ parentId: parentTaskId });
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Wait for a task to complete
|
|
249
|
+
*/
|
|
250
|
+
export async function waitForTask(taskId, options) {
|
|
251
|
+
const timeout = options?.timeout ?? 5 * 60 * 1000; // 5 minutes
|
|
252
|
+
const pollInterval = options?.pollInterval ?? 1000; // 1 second
|
|
253
|
+
const startTime = Date.now();
|
|
254
|
+
while (Date.now() - startTime < timeout) {
|
|
255
|
+
const task = await taskQueue.get(taskId);
|
|
256
|
+
if (!task) {
|
|
257
|
+
return {
|
|
258
|
+
taskId,
|
|
259
|
+
success: false,
|
|
260
|
+
error: {
|
|
261
|
+
code: 'TASK_NOT_FOUND',
|
|
262
|
+
message: `Task "${taskId}" not found`,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (task.status === 'completed') {
|
|
267
|
+
return {
|
|
268
|
+
taskId,
|
|
269
|
+
success: true,
|
|
270
|
+
output: task.output,
|
|
271
|
+
metadata: {
|
|
272
|
+
duration: task.completedAt && task.startedAt
|
|
273
|
+
? task.completedAt.getTime() - task.startedAt.getTime()
|
|
274
|
+
: 0,
|
|
275
|
+
startedAt: task.startedAt || task.createdAt,
|
|
276
|
+
completedAt: task.completedAt || new Date(),
|
|
277
|
+
worker: task.assignment?.worker,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (task.status === 'failed') {
|
|
282
|
+
return {
|
|
283
|
+
taskId,
|
|
284
|
+
success: false,
|
|
285
|
+
error: {
|
|
286
|
+
code: 'TASK_FAILED',
|
|
287
|
+
message: task.error || 'Task failed',
|
|
288
|
+
},
|
|
289
|
+
metadata: {
|
|
290
|
+
duration: task.completedAt && task.startedAt
|
|
291
|
+
? task.completedAt.getTime() - task.startedAt.getTime()
|
|
292
|
+
: 0,
|
|
293
|
+
startedAt: task.startedAt || task.createdAt,
|
|
294
|
+
completedAt: task.completedAt || new Date(),
|
|
295
|
+
worker: task.assignment?.worker,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (task.status === 'cancelled') {
|
|
300
|
+
return {
|
|
301
|
+
taskId,
|
|
302
|
+
success: false,
|
|
303
|
+
error: {
|
|
304
|
+
code: 'TASK_CANCELLED',
|
|
305
|
+
message: 'Task was cancelled',
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Wait before polling again
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
taskId,
|
|
314
|
+
success: false,
|
|
315
|
+
error: {
|
|
316
|
+
code: 'TIMEOUT',
|
|
317
|
+
message: `Task did not complete within ${timeout}ms`,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for digital-tasks
|
|
3
|
+
*
|
|
4
|
+
* Task = Function + metadata (status, progress, assignment, dependencies)
|
|
5
|
+
*
|
|
6
|
+
* Every task is a function call. The function can be:
|
|
7
|
+
* - Code: generates executable code
|
|
8
|
+
* - Generative: AI generates content (no tools)
|
|
9
|
+
* - Agentic: AI with tools in a loop
|
|
10
|
+
* - Human: requires human input
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
export {};
|