digital-tasks 2.1.3 → 2.4.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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +560 -0
  4. package/dist/client.d.ts +202 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +85 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/index.d.ts +9 -7
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +8 -6
  11. package/dist/index.js.map +1 -1
  12. package/dist/markdown.d.ts.map +1 -1
  13. package/dist/markdown.js +48 -27
  14. package/dist/markdown.js.map +1 -1
  15. package/dist/project.d.ts.map +1 -1
  16. package/dist/project.js +48 -30
  17. package/dist/project.js.map +1 -1
  18. package/dist/queue.d.ts.map +1 -1
  19. package/dist/queue.js +70 -31
  20. package/dist/queue.js.map +1 -1
  21. package/dist/task.d.ts +1 -1
  22. package/dist/task.d.ts.map +1 -1
  23. package/dist/task.js +122 -60
  24. package/dist/task.js.map +1 -1
  25. package/dist/types.d.ts +135 -22
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +10 -2
  28. package/dist/types.js.map +1 -1
  29. package/dist/worker.d.ts +264 -0
  30. package/dist/worker.d.ts.map +1 -0
  31. package/dist/worker.js +656 -0
  32. package/dist/worker.js.map +1 -0
  33. package/package.json +29 -14
  34. package/src/client.ts +268 -0
  35. package/src/index.ts +12 -10
  36. package/src/markdown.ts +63 -48
  37. package/src/project.ts +57 -42
  38. package/src/queue.ts +76 -37
  39. package/src/task.ts +132 -75
  40. package/src/types.ts +177 -40
  41. package/src/worker.ts +959 -0
  42. package/test/project.test.ts +28 -84
  43. package/test/queue.test.ts +51 -24
  44. package/test/task.test.ts +80 -27
  45. package/test/worker.test.ts +1158 -0
  46. package/tsconfig.json +2 -13
  47. package/vitest.config.ts +48 -0
  48. package/wrangler.jsonc +44 -0
  49. package/LICENSE +0 -21
  50. package/dist/function-task.d.ts +0 -319
  51. package/dist/function-task.d.ts.map +0 -1
  52. package/dist/function-task.js +0 -286
  53. package/dist/function-task.js.map +0 -1
  54. package/src/index.js +0 -73
  55. package/src/markdown.js +0 -509
  56. package/src/project.js +0 -396
  57. package/src/queue.js +0 -346
  58. package/src/task.js +0 -320
  59. package/src/types.js +0 -14
package/src/markdown.ts CHANGED
@@ -49,6 +49,7 @@ import type {
49
49
  ParallelGroup,
50
50
  SequentialGroup,
51
51
  ExecutionMode,
52
+ CreateProjectOptions,
52
53
  } from './project.js'
53
54
  import { task, parallel, sequential, createProject } from './project.js'
54
55
 
@@ -61,8 +62,8 @@ import { task, parallel, sequential, createProject } from './project.js'
61
62
  */
62
63
  const STATUS_MARKERS: Record<string, TaskStatus> = {
63
64
  ' ': 'pending',
64
- 'x': 'completed',
65
- 'X': 'completed',
65
+ x: 'completed',
66
+ X: 'completed',
66
67
  '-': 'in_progress',
67
68
  '~': 'blocked',
68
69
  '!': 'failed',
@@ -93,7 +94,7 @@ const PRIORITY_MARKERS: Record<string, TaskPriority> = {
93
94
  '!': 'urgent',
94
95
  '^': 'high',
95
96
  '': 'normal',
96
- 'v': 'low',
97
+ v: 'low',
97
98
  }
98
99
 
99
100
  // ============================================================================
@@ -135,20 +136,23 @@ function parseLine(line: string): ParsedLine {
135
136
  const raw = line
136
137
 
137
138
  // Count leading spaces for indent (2 spaces = 1 level)
138
- const leadingSpaces = line.match(/^(\s*)/)?.[1].length || 0
139
+ const leadingSpacesMatch = line.match(/^(\s*)/)
140
+ const leadingSpaces = leadingSpacesMatch?.[1]?.length ?? 0
139
141
  const indent = Math.floor(leadingSpaces / 2)
140
142
  const trimmed = line.slice(leadingSpaces)
141
143
 
142
144
  // Check for heading
143
145
  const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/)
144
146
  if (headingMatch) {
147
+ const matchedHashes = headingMatch[1]!
148
+ const matchedTitle = headingMatch[2]!
145
149
  return {
146
150
  indent,
147
151
  isTask: false,
148
152
  isOrdered: false,
149
- title: headingMatch[2].trim(),
153
+ title: matchedTitle.trim(),
150
154
  isHeading: true,
151
- headingLevel: headingMatch[1].length,
155
+ headingLevel: matchedHashes.length,
152
156
  raw,
153
157
  }
154
158
  }
@@ -156,8 +160,8 @@ function parseLine(line: string): ParsedLine {
156
160
  // Check for unordered task: - [ ] or - [x] etc.
157
161
  const unorderedMatch = trimmed.match(/^[-*]\s+\[([^\]]*)\]\s*(.*)$/)
158
162
  if (unorderedMatch) {
159
- const marker = unorderedMatch[1]
160
- let title = unorderedMatch[2].trim()
163
+ const marker = unorderedMatch[1]!
164
+ let title = unorderedMatch[2]!.trim()
161
165
  let priority: TaskPriority = 'normal'
162
166
 
163
167
  // Check for priority marker at start of title
@@ -179,7 +183,7 @@ function parseLine(line: string): ParsedLine {
179
183
  indent,
180
184
  isTask: true,
181
185
  isOrdered: false,
182
- status: STATUS_MARKERS[marker] || 'pending',
186
+ status: STATUS_MARKERS[marker] ?? 'pending',
183
187
  priority,
184
188
  title,
185
189
  isHeading: false,
@@ -190,8 +194,9 @@ function parseLine(line: string): ParsedLine {
190
194
  // Check for ordered task: 1. [ ] or 1. [x] etc.
191
195
  const orderedMatch = trimmed.match(/^(\d+)\.\s+\[([^\]]*)\]\s*(.*)$/)
192
196
  if (orderedMatch) {
193
- const marker = orderedMatch[2]
194
- let title = orderedMatch[3].trim()
197
+ const orderNum = orderedMatch[1]!
198
+ const marker = orderedMatch[2]!
199
+ let title = orderedMatch[3]!.trim()
195
200
  let priority: TaskPriority = 'normal'
196
201
 
197
202
  // Check for priority marker
@@ -213,8 +218,8 @@ function parseLine(line: string): ParsedLine {
213
218
  indent,
214
219
  isTask: true,
215
220
  isOrdered: true,
216
- orderNumber: parseInt(orderedMatch[1], 10),
217
- status: STATUS_MARKERS[marker] || 'pending',
221
+ orderNumber: parseInt(orderNum, 10),
222
+ status: STATUS_MARKERS[marker] ?? 'pending',
218
223
  priority,
219
224
  title,
220
225
  isHeading: false,
@@ -247,7 +252,7 @@ function parseTasksAtIndent(
247
252
  let modeSet = false
248
253
 
249
254
  while (index < lines.length) {
250
- const line = lines[index]
255
+ const line = lines[index]!
251
256
 
252
257
  // Stop if we've gone back to a lower indent level
253
258
  if (line.indent < baseIndent && (line.isTask || line.isHeading)) {
@@ -269,20 +274,21 @@ function parseTasksAtIndent(
269
274
  }
270
275
 
271
276
  // Parse subtasks
272
- const { tasks: subtasks, nextIndex } = parseTasksAtIndent(
273
- lines,
274
- index + 1,
275
- baseIndent + 1
276
- )
277
-
278
- const taskDef = task(line.title, {
279
- priority: line.priority,
280
- subtasks: subtasks.length > 0 ? subtasks : undefined,
277
+ const { tasks: subtasks, nextIndex } = parseTasksAtIndent(lines, index + 1, baseIndent + 1)
278
+
279
+ const taskOptions: Partial<Omit<TaskDefinition, '__type' | 'title'>> = {
281
280
  metadata: {
282
281
  _originalStatus: line.status,
283
282
  _lineNumber: index,
284
283
  },
285
- })
284
+ }
285
+ if (line.priority !== undefined) {
286
+ taskOptions.priority = line.priority
287
+ }
288
+ if (subtasks.length > 0) {
289
+ taskOptions.subtasks = subtasks
290
+ }
291
+ const taskDef = task(line.title, taskOptions)
286
292
 
287
293
  tasks.push(taskDef)
288
294
  index = nextIndex
@@ -324,9 +330,9 @@ export function parseMarkdown(markdown: string): Project {
324
330
  const allTasks: TaskNode[] = []
325
331
 
326
332
  // Find project name from first h1
327
- const h1Index = lines.findIndex(l => l.isHeading && l.headingLevel === 1)
333
+ const h1Index = lines.findIndex((l) => l.isHeading && l.headingLevel === 1)
328
334
  if (h1Index !== -1) {
329
- projectName = lines[h1Index].title
335
+ projectName = lines[h1Index]!.title
330
336
  }
331
337
 
332
338
  // Process sections and tasks
@@ -334,7 +340,7 @@ export function parseMarkdown(markdown: string): Project {
334
340
  let index = 0
335
341
 
336
342
  while (index < lines.length) {
337
- const line = lines[index]
343
+ const line = lines[index]!
338
344
 
339
345
  // New section (h2)
340
346
  if (line.isHeading && line.headingLevel === 2) {
@@ -353,7 +359,7 @@ export function parseMarkdown(markdown: string): Project {
353
359
 
354
360
  const modeMatch = sectionName.match(/\((parallel|sequential)\)\s*$/i)
355
361
  if (modeMatch) {
356
- sectionMode = modeMatch[1].toLowerCase() as ExecutionMode
362
+ sectionMode = modeMatch[1]!.toLowerCase() as ExecutionMode
357
363
  sectionName = sectionName.replace(/\s*\((parallel|sequential)\)\s*$/i, '')
358
364
  }
359
365
 
@@ -396,11 +402,15 @@ export function parseMarkdown(markdown: string): Project {
396
402
  }
397
403
  }
398
404
 
399
- return createProject({
405
+ const projectOptions: CreateProjectOptions = {
400
406
  name: projectName,
401
- description: projectDescription,
402
407
  tasks: allTasks,
403
- })
408
+ }
409
+ if (projectDescription !== undefined) {
410
+ projectOptions.description = projectDescription
411
+ }
412
+
413
+ return createProject(projectOptions)
404
414
  }
405
415
 
406
416
  // ============================================================================
@@ -435,13 +445,13 @@ function serializeTaskNode(
435
445
 
436
446
  if (node.__type === 'task') {
437
447
  const taskDef = node as TaskDefinition
438
- const status = (taskDef.metadata?._originalStatus as TaskStatus) || 'pending'
448
+ const status = (taskDef.metadata?.['_originalStatus'] as TaskStatus) || 'pending'
439
449
  const marker = options.includeStatus !== false ? STATUS_TO_MARKER[status] : ' '
440
450
 
441
451
  let prefix: string
442
452
  if (isSequential) {
443
453
  // Use numbered list for sequential
444
- const num = (taskDef.metadata?._sequenceNumber as number) || 1
454
+ const num = (taskDef.metadata?.['_sequenceNumber'] as number) || 1
445
455
  prefix = `${num}. [${marker}]`
446
456
  } else {
447
457
  // Use bullet for parallel
@@ -450,11 +460,16 @@ function serializeTaskNode(
450
460
 
451
461
  let title = taskDef.title
452
462
  if (options.includePriority && taskDef.priority && taskDef.priority !== 'normal') {
453
- const priorityMarker = taskDef.priority === 'critical' ? '!!'
454
- : taskDef.priority === 'urgent' ? '!'
455
- : taskDef.priority === 'high' ? '^'
456
- : taskDef.priority === 'low' ? 'v'
457
- : ''
463
+ const priorityMarker =
464
+ taskDef.priority === 'critical'
465
+ ? '!!'
466
+ : taskDef.priority === 'urgent'
467
+ ? '!'
468
+ : taskDef.priority === 'high'
469
+ ? '^'
470
+ : taskDef.priority === 'low'
471
+ ? 'v'
472
+ : ''
458
473
  title = `${priorityMarker}${title}`
459
474
  }
460
475
 
@@ -471,7 +486,7 @@ function serializeTaskNode(
471
486
  let seqNum = 1
472
487
  for (const child of group.tasks) {
473
488
  if (child.__type === 'task') {
474
- (child as TaskDefinition).metadata = {
489
+ ;(child as TaskDefinition).metadata = {
475
490
  ...(child as TaskDefinition).metadata,
476
491
  _sequenceNumber: seqNum++,
477
492
  }
@@ -483,7 +498,7 @@ function serializeTaskNode(
483
498
  let seqNum = 1
484
499
  for (const child of group.tasks) {
485
500
  if (child.__type === 'task') {
486
- (child as TaskDefinition).metadata = {
501
+ ;(child as TaskDefinition).metadata = {
487
502
  ...(child as TaskDefinition).metadata,
488
503
  _sequenceNumber: seqNum++,
489
504
  }
@@ -558,10 +573,7 @@ export function toMarkdown(project: Project, options: SerializeOptions = {}): st
558
573
  * Update task statuses in a project from markdown
559
574
  * (Useful for syncing when markdown is edited externally)
560
575
  */
561
- export function syncStatusFromMarkdown(
562
- project: Project,
563
- markdown: string
564
- ): Project {
576
+ export function syncStatusFromMarkdown(project: Project, markdown: string): Project {
565
577
  const parsed = parseMarkdown(markdown)
566
578
 
567
579
  // Build a map of task titles to statuses from parsed markdown
@@ -570,7 +582,7 @@ export function syncStatusFromMarkdown(
570
582
  function collectStatuses(node: TaskNode) {
571
583
  if (node.__type === 'task') {
572
584
  const taskDef = node as TaskDefinition
573
- const status = taskDef.metadata?._originalStatus as TaskStatus
585
+ const status = taskDef.metadata?.['_originalStatus'] as TaskStatus
574
586
  if (status) {
575
587
  statusMap.set(taskDef.title, status)
576
588
  }
@@ -590,14 +602,17 @@ export function syncStatusFromMarkdown(
590
602
  if (node.__type === 'task') {
591
603
  const taskDef = node as TaskDefinition
592
604
  const newStatus = statusMap.get(taskDef.title)
593
- return {
605
+ const result: TaskDefinition = {
594
606
  ...taskDef,
595
607
  metadata: {
596
608
  ...taskDef.metadata,
597
- _originalStatus: newStatus || taskDef.metadata?._originalStatus,
609
+ _originalStatus: newStatus || taskDef.metadata?.['_originalStatus'],
598
610
  },
599
- subtasks: taskDef.subtasks?.map(updateStatuses),
600
611
  }
612
+ if (taskDef.subtasks) {
613
+ result.subtasks = taskDef.subtasks.map(updateStatuses)
614
+ }
615
+ return result
601
616
  } else if (node.__type === 'parallel') {
602
617
  const group = node as ParallelGroup
603
618
  return {
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