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/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 {};