digital-tasks 2.1.1 → 2.3.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +560 -0
  3. package/package.json +20 -5
  4. package/src/client.ts +268 -0
  5. package/src/index.ts +12 -10
  6. package/src/markdown.ts +63 -48
  7. package/src/project.ts +57 -42
  8. package/src/queue.ts +76 -37
  9. package/src/task.ts +132 -75
  10. package/src/types.ts +177 -40
  11. package/src/worker.ts +959 -0
  12. package/test/project.test.ts +28 -84
  13. package/test/queue.test.ts +51 -24
  14. package/test/task.test.ts +80 -27
  15. package/test/worker.test.ts +1158 -0
  16. package/tsconfig.json +2 -13
  17. package/vitest.config.ts +48 -0
  18. package/wrangler.jsonc +44 -0
  19. package/.turbo/turbo-build.log +0 -5
  20. package/dist/function-task.d.ts +0 -319
  21. package/dist/function-task.d.ts.map +0 -1
  22. package/dist/function-task.js +0 -286
  23. package/dist/function-task.js.map +0 -1
  24. package/dist/index.d.ts +0 -72
  25. package/dist/index.d.ts.map +0 -1
  26. package/dist/index.js +0 -74
  27. package/dist/index.js.map +0 -1
  28. package/dist/markdown.d.ts +0 -112
  29. package/dist/markdown.d.ts.map +0 -1
  30. package/dist/markdown.js +0 -510
  31. package/dist/markdown.js.map +0 -1
  32. package/dist/project.d.ts +0 -259
  33. package/dist/project.d.ts.map +0 -1
  34. package/dist/project.js +0 -397
  35. package/dist/project.js.map +0 -1
  36. package/dist/queue.d.ts +0 -17
  37. package/dist/queue.d.ts.map +0 -1
  38. package/dist/queue.js +0 -347
  39. package/dist/queue.js.map +0 -1
  40. package/dist/task.d.ts +0 -69
  41. package/dist/task.d.ts.map +0 -1
  42. package/dist/task.js +0 -321
  43. package/dist/task.js.map +0 -1
  44. package/dist/types.d.ts +0 -292
  45. package/dist/types.d.ts.map +0 -1
  46. package/dist/types.js +0 -15
  47. package/dist/types.js.map +0 -1
  48. package/src/index.js +0 -73
  49. package/src/markdown.js +0 -509
  50. package/src/project.js +0 -396
  51. package/src/queue.js +0 -346
  52. package/src/task.js +0 -320
  53. package/src/types.js +0 -14
package/src/project.ts CHANGED
@@ -59,10 +59,7 @@ export type ExecutionMode = 'parallel' | 'sequential'
59
59
  /**
60
60
  * Task node in a workflow - can be a single task or a group
61
61
  */
62
- export type TaskNode =
63
- | TaskDefinition
64
- | ParallelGroup
65
- | SequentialGroup
62
+ export type TaskNode = TaskDefinition | ParallelGroup | SequentialGroup
66
63
 
67
64
  /**
68
65
  * Function type for the DSL
@@ -107,12 +104,7 @@ export interface SequentialGroup {
107
104
  /**
108
105
  * Project status
109
106
  */
110
- export type ProjectStatus =
111
- | 'draft'
112
- | 'active'
113
- | 'paused'
114
- | 'completed'
115
- | 'cancelled'
107
+ export type ProjectStatus = 'draft' | 'active' | 'paused' | 'completed' | 'cancelled'
116
108
 
117
109
  /**
118
110
  * Project definition
@@ -261,19 +253,28 @@ function generateProjectId(): string {
261
253
  */
262
254
  export function createProject(options: CreateProjectOptions): Project {
263
255
  const now = new Date()
264
- return {
256
+ const project: Project = {
265
257
  id: generateProjectId(),
266
258
  name: options.name,
267
- description: options.description,
268
259
  status: 'draft',
269
260
  tasks: options.tasks || [],
270
261
  defaultMode: options.defaultMode || 'sequential',
271
- owner: options.owner,
272
- tags: options.tags,
273
262
  createdAt: now,
274
263
  updatedAt: now,
275
- metadata: options.metadata,
276
264
  }
265
+ if (options.description !== undefined) {
266
+ project.description = options.description
267
+ }
268
+ if (options.owner !== undefined) {
269
+ project.owner = options.owner
270
+ }
271
+ if (options.tags !== undefined) {
272
+ project.tags = options.tags
273
+ }
274
+ if (options.metadata !== undefined) {
275
+ project.metadata = options.metadata
276
+ }
277
+ return project
277
278
  }
278
279
 
279
280
  // ============================================================================
@@ -325,7 +326,10 @@ export function workflow(name: string, description?: string) {
325
326
  */
326
327
  then(...nodes: TaskNode[]) {
327
328
  if (nodes.length === 1) {
328
- tasks.push(nodes[0])
329
+ const node = nodes[0]
330
+ if (node !== undefined) {
331
+ tasks.push(node)
332
+ }
329
333
  } else {
330
334
  tasks.push(sequential(...nodes))
331
335
  }
@@ -344,12 +348,15 @@ export function workflow(name: string, description?: string) {
344
348
  * Build the project
345
349
  */
346
350
  build(options?: Partial<Omit<CreateProjectOptions, 'name' | 'tasks'>>): Project {
347
- return createProject({
351
+ const projectOptions: CreateProjectOptions = {
348
352
  name,
349
- description,
350
353
  tasks,
351
354
  ...options,
352
- })
355
+ }
356
+ if (description !== undefined) {
357
+ projectOptions.description = description
358
+ }
359
+ return createProject(projectOptions)
353
360
  },
354
361
  }
355
362
 
@@ -380,9 +387,7 @@ export async function materializeProject(
380
387
  const taskId = `${project.id}_task_${taskIndex++}`
381
388
 
382
389
  // Create dependencies based on mode (as string array for CreateTaskOptions)
383
- const dependencies = mode === 'sequential' && previousIds.length > 0
384
- ? previousIds
385
- : undefined
390
+ const dependencies = mode === 'sequential' && previousIds.length > 0 ? previousIds : undefined
386
391
 
387
392
  // Create a FunctionDefinition from the task definition
388
393
  // Default to generative function type for DSL tasks
@@ -394,19 +399,28 @@ export async function materializeProject(
394
399
  output: 'string',
395
400
  } as FunctionDefinition
396
401
 
397
- const newTask = await createBaseTask({
398
- function: functionDef,
402
+ const createTaskOptions: CreateTaskOptions = {
403
+ tool: functionDef,
399
404
  priority: taskDef.priority || 'normal',
400
- assignTo: taskDef.assignTo,
401
- tags: taskDef.tags,
402
- parentId,
403
405
  projectId: project.id,
404
- dependencies,
405
406
  metadata: {
406
407
  ...taskDef.metadata,
407
408
  _taskNodeIndex: taskIndex - 1,
408
409
  },
409
- })
410
+ }
411
+ if (taskDef.assignTo !== undefined) {
412
+ createTaskOptions.assignTo = taskDef.assignTo
413
+ }
414
+ if (taskDef.tags !== undefined) {
415
+ createTaskOptions.tags = taskDef.tags
416
+ }
417
+ if (parentId !== undefined) {
418
+ createTaskOptions.parentId = parentId
419
+ }
420
+ if (dependencies !== undefined) {
421
+ createTaskOptions.dependencies = dependencies
422
+ }
423
+ const newTask = await createBaseTask(createTaskOptions)
410
424
 
411
425
  // Override the generated ID with our predictable one
412
426
  ;(newTask as AnyTask).id = taskId
@@ -455,7 +469,12 @@ export async function materializeProject(
455
469
  // Process all root-level tasks
456
470
  let previousIds: string[] = []
457
471
  for (const node of project.tasks) {
458
- previousIds = await processNode(node, undefined, previousIds, project.defaultMode || 'sequential')
472
+ previousIds = await processNode(
473
+ node,
474
+ undefined,
475
+ previousIds,
476
+ project.defaultMode || 'sequential'
477
+ )
459
478
  }
460
479
 
461
480
  return { project, tasks }
@@ -469,8 +488,8 @@ export async function materializeProject(
469
488
  * Get all tasks that depend on a given task (dependants)
470
489
  */
471
490
  export function getDependants(taskId: string, allTasks: AnyTask[]): AnyTask[] {
472
- return allTasks.filter(t =>
473
- t.dependencies?.some(d => d.taskId === taskId && d.type === 'blocked_by')
491
+ return allTasks.filter((t) =>
492
+ t.dependencies?.some((d) => d.taskId === taskId && d.type === 'blocked_by')
474
493
  )
475
494
  }
476
495
 
@@ -480,25 +499,21 @@ export function getDependants(taskId: string, allTasks: AnyTask[]): AnyTask[] {
480
499
  export function getDependencies(task: AnyTask, allTasks: AnyTask[]): AnyTask[] {
481
500
  if (!task.dependencies) return []
482
501
 
483
- const depIds = task.dependencies
484
- .filter(d => d.type === 'blocked_by')
485
- .map(d => d.taskId)
502
+ const depIds = task.dependencies.filter((d) => d.type === 'blocked_by').map((d) => d.taskId)
486
503
 
487
- return allTasks.filter(t => depIds.includes(t.id))
504
+ return allTasks.filter((t) => depIds.includes(t.id))
488
505
  }
489
506
 
490
507
  /**
491
508
  * Get tasks that are ready to execute (no unsatisfied dependencies)
492
509
  */
493
510
  export function getReadyTasks(allTasks: AnyTask[]): AnyTask[] {
494
- return allTasks.filter(t => {
511
+ return allTasks.filter((t) => {
495
512
  if (t.status !== 'queued' && t.status !== 'pending') return false
496
513
 
497
514
  if (!t.dependencies || t.dependencies.length === 0) return true
498
515
 
499
- return t.dependencies
500
- .filter(d => d.type === 'blocked_by')
501
- .every(d => d.satisfied)
516
+ return t.dependencies.filter((d) => d.type === 'blocked_by').every((d) => d.satisfied)
502
517
  })
503
518
  }
504
519
 
@@ -513,7 +528,7 @@ export function hasCycles(allTasks: AnyTask[]): boolean {
513
528
  visited.add(taskId)
514
529
  recStack.add(taskId)
515
530
 
516
- const task = allTasks.find(t => t.id === taskId)
531
+ const task = allTasks.find((t) => t.id === taskId)
517
532
  if (task?.dependencies) {
518
533
  for (const dep of task.dependencies) {
519
534
  if (dep.type === 'blocked_by') {
@@ -554,7 +569,7 @@ export function sortTasks(allTasks: AnyTask[]): AnyTask[] {
554
569
  if (task.dependencies) {
555
570
  for (const dep of task.dependencies) {
556
571
  if (dep.type === 'blocked_by') {
557
- const depTask = allTasks.find(t => t.id === dep.taskId)
572
+ const depTask = allTasks.find((t) => t.id === dep.taskId)
558
573
  if (depTask) visit(depTask)
559
574
  }
560
575
  }
package/src/queue.ts CHANGED
@@ -49,12 +49,14 @@ class InMemoryTaskQueue implements TaskQueue {
49
49
  }
50
50
 
51
51
  async add(task: AnyTask): Promise<void> {
52
+ // Read the underlying Tool, tolerating the legacy `function` alias.
53
+ const tool = task.tool ?? task.function
52
54
  // Add created event
53
55
  const event: TaskEvent = {
54
56
  id: `evt_${Date.now()}`,
55
57
  type: 'created',
56
58
  timestamp: new Date(),
57
- message: `Task created: ${task.function.name}`,
59
+ message: `Task created: ${tool?.name ?? task.id}`,
58
60
  }
59
61
 
60
62
  const taskWithEvent = {
@@ -88,10 +90,16 @@ class InMemoryTaskQueue implements TaskQueue {
88
90
  if (options.status && options.status !== task.status) {
89
91
  events.push({
90
92
  id: `evt_${Date.now()}_status`,
91
- type: options.status === 'completed' ? 'completed' :
92
- options.status === 'failed' ? 'failed' :
93
- options.status === 'in_progress' ? 'started' :
94
- options.status === 'blocked' ? 'blocked' : 'progress',
93
+ type:
94
+ options.status === 'completed'
95
+ ? 'completed'
96
+ : options.status === 'failed'
97
+ ? 'failed'
98
+ : options.status === 'in_progress'
99
+ ? 'started'
100
+ : options.status === 'blocked'
101
+ ? 'blocked'
102
+ : 'progress',
95
103
  timestamp: new Date(),
96
104
  message: `Status changed from ${task.status} to ${options.status}`,
97
105
  })
@@ -102,12 +110,25 @@ class InMemoryTaskQueue implements TaskQueue {
102
110
  if (options.progress) {
103
111
  progressUpdate = {
104
112
  percent: options.progress.percent ?? task.progress?.percent ?? 0,
105
- step: options.progress.step ?? task.progress?.step,
106
- totalSteps: options.progress.totalSteps ?? task.progress?.totalSteps,
107
- currentStep: options.progress.currentStep ?? task.progress?.currentStep,
108
- estimatedTimeRemaining: options.progress.estimatedTimeRemaining ?? task.progress?.estimatedTimeRemaining,
109
113
  updatedAt: new Date(),
110
114
  }
115
+ const step = options.progress.step ?? task.progress?.step
116
+ if (step !== undefined) {
117
+ progressUpdate.step = step
118
+ }
119
+ const totalSteps = options.progress.totalSteps ?? task.progress?.totalSteps
120
+ if (totalSteps !== undefined) {
121
+ progressUpdate.totalSteps = totalSteps
122
+ }
123
+ const currentStep = options.progress.currentStep ?? task.progress?.currentStep
124
+ if (currentStep !== undefined) {
125
+ progressUpdate.currentStep = currentStep
126
+ }
127
+ const estTime =
128
+ options.progress.estimatedTimeRemaining ?? task.progress?.estimatedTimeRemaining
129
+ if (estTime !== undefined) {
130
+ progressUpdate.estimatedTimeRemaining = estTime
131
+ }
111
132
  }
112
133
 
113
134
  const updated: AnyTask = {
@@ -145,9 +166,9 @@ class InMemoryTaskQueue implements TaskQueue {
145
166
  results = results.filter((t) => priorities.includes(t.priority))
146
167
  }
147
168
 
148
- // Filter by function type
169
+ // Filter by function type (reads `tool` with fallback to legacy `function`)
149
170
  if (options.functionType) {
150
- results = results.filter((t) => t.function.type === options.functionType)
171
+ results = results.filter((t) => (t.tool ?? t.function)?.type === options.functionType)
151
172
  }
152
173
 
153
174
  // Filter by assigned worker
@@ -157,9 +178,7 @@ class InMemoryTaskQueue implements TaskQueue {
157
178
 
158
179
  // Filter by tags
159
180
  if (options.tags && options.tags.length > 0) {
160
- results = results.filter(
161
- (t) => t.tags && options.tags!.some((tag) => t.tags!.includes(tag))
162
- )
181
+ results = results.filter((t) => t.tags && options.tags!.some((tag) => t.tags!.includes(tag)))
163
182
  }
164
183
 
165
184
  // Filter by project
@@ -172,14 +191,17 @@ class InMemoryTaskQueue implements TaskQueue {
172
191
  results = results.filter((t) => t.parentId === options.parentId)
173
192
  }
174
193
 
175
- // Text search
194
+ // Text search (reads `tool` with fallback to legacy `function`)
176
195
  if (options.search) {
177
196
  const search = options.search.toLowerCase()
178
- results = results.filter(
179
- (t) =>
180
- t.function.name.toLowerCase().includes(search) ||
181
- t.function.description?.toLowerCase().includes(search)
182
- )
197
+ results = results.filter((t) => {
198
+ const tool = t.tool ?? t.function
199
+ if (!tool) return false
200
+ return (
201
+ tool.name.toLowerCase().includes(search) ||
202
+ tool.description?.toLowerCase().includes(search)
203
+ )
204
+ })
183
205
  }
184
206
 
185
207
  // Sort
@@ -232,7 +254,11 @@ class InMemoryTaskQueue implements TaskQueue {
232
254
  // Find first task the worker can handle
233
255
  for (const task of queuedTasks) {
234
256
  // Check if worker type is allowed
235
- if (task.allowedWorkers && !task.allowedWorkers.includes(worker.type) && !task.allowedWorkers.includes('any')) {
257
+ if (
258
+ task.allowedWorkers &&
259
+ !task.allowedWorkers.includes(worker.type) &&
260
+ !task.allowedWorkers.includes('any')
261
+ ) {
236
262
  continue
237
263
  }
238
264
 
@@ -289,14 +315,17 @@ class InMemoryTaskQueue implements TaskQueue {
289
315
  const task = await this.get(taskId)
290
316
  if (!task) return
291
317
 
318
+ const event: Omit<TaskEvent, 'id' | 'timestamp'> = {
319
+ type: 'completed',
320
+ message: 'Task completed successfully',
321
+ data: { output },
322
+ }
323
+ if (task.assignment?.worker !== undefined) {
324
+ event.actor = task.assignment.worker
325
+ }
292
326
  await this.update(taskId, {
293
327
  status: 'completed',
294
- event: {
295
- type: 'completed',
296
- actor: task.assignment?.worker,
297
- message: 'Task completed successfully',
298
- data: { output },
299
- },
328
+ event,
300
329
  })
301
330
 
302
331
  // Update task output separately since it's not in UpdateTaskOptions
@@ -328,7 +357,9 @@ class InMemoryTaskQueue implements TaskQueue {
328
357
  })
329
358
 
330
359
  // Unblock if all dependencies satisfied
331
- const allSatisfied = updatedDeps.filter((d) => d.type === 'blocked_by').every((d) => d.satisfied)
360
+ const allSatisfied = updatedDeps
361
+ .filter((d) => d.type === 'blocked_by')
362
+ .every((d) => d.satisfied)
332
363
  if (allSatisfied && otherTask.status === 'blocked') {
333
364
  await this.update(otherTask.id, {
334
365
  status: 'queued',
@@ -347,14 +378,17 @@ class InMemoryTaskQueue implements TaskQueue {
347
378
  const task = await this.get(taskId)
348
379
  if (!task) return
349
380
 
381
+ const event: Omit<TaskEvent, 'id' | 'timestamp'> = {
382
+ type: 'failed',
383
+ message: `Task failed: ${error}`,
384
+ data: { error },
385
+ }
386
+ if (task.assignment?.worker !== undefined) {
387
+ event.actor = task.assignment.worker
388
+ }
350
389
  await this.update(taskId, {
351
390
  status: 'failed',
352
- event: {
353
- type: 'failed',
354
- actor: task.assignment?.worker,
355
- message: `Task failed: ${error}`,
356
- data: { error },
357
- },
391
+ event,
358
392
  })
359
393
  }
360
394
 
@@ -401,13 +435,18 @@ class InMemoryTaskQueue implements TaskQueue {
401
435
  }
402
436
  }
403
437
 
404
- return {
438
+ const stats: TaskQueueStats = {
405
439
  total: tasks.length,
406
440
  byStatus,
407
441
  byPriority,
408
- avgWaitTime: waitTimeCount > 0 ? totalWaitTime / waitTimeCount : undefined,
409
- avgCompletionTime: completionTimeCount > 0 ? totalCompletionTime / completionTimeCount : undefined,
410
442
  }
443
+ if (waitTimeCount > 0) {
444
+ stats.avgWaitTime = totalWaitTime / waitTimeCount
445
+ }
446
+ if (completionTimeCount > 0) {
447
+ stats.avgCompletionTime = totalCompletionTime / completionTimeCount
448
+ }
449
+ return stats
411
450
  }
412
451
  }
413
452