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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/README.md +560 -0
- package/dist/client.d.ts +202 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +85 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +48 -27
- package/dist/markdown.js.map +1 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +48 -30
- package/dist/project.js.map +1 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +70 -31
- package/dist/queue.js.map +1 -1
- package/dist/task.d.ts +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +122 -60
- package/dist/task.js.map +1 -1
- package/dist/types.d.ts +135 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -2
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +264 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +656 -0
- package/dist/worker.js.map +1 -0
- package/package.json +29 -14
- package/src/client.ts +268 -0
- package/src/index.ts +12 -10
- package/src/markdown.ts +63 -48
- package/src/project.ts +57 -42
- package/src/queue.ts +76 -37
- package/src/task.ts +132 -75
- package/src/types.ts +177 -40
- package/src/worker.ts +959 -0
- package/test/project.test.ts +28 -84
- package/test/queue.test.ts +51 -24
- package/test/task.test.ts +80 -27
- package/test/worker.test.ts +1158 -0
- package/tsconfig.json +2 -13
- package/vitest.config.ts +48 -0
- package/wrangler.jsonc +44 -0
- package/LICENSE +0 -21
- package/dist/function-task.d.ts +0 -319
- package/dist/function-task.d.ts.map +0 -1
- package/dist/function-task.js +0 -286
- package/dist/function-task.js.map +0 -1
- package/src/index.js +0 -73
- package/src/markdown.js +0 -509
- package/src/project.js +0 -396
- package/src/queue.js +0 -346
- package/src/task.js +0 -320
- 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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
153
|
+
title: matchedTitle.trim(),
|
|
150
154
|
isHeading: true,
|
|
151
|
-
headingLevel:
|
|
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]
|
|
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]
|
|
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
|
|
194
|
-
|
|
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(
|
|
217
|
-
status: STATUS_MARKERS[marker]
|
|
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
|
-
|
|
274
|
-
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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 =
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
398
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|