zayra-task-engine 0.0.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/README.md +268 -0
- package/package.json +32 -0
- package/src/errors.js +62 -0
- package/src/events.js +124 -0
- package/src/executor.js +110 -0
- package/src/index.js +23 -0
- package/src/manager.js +328 -0
- package/src/permissions.js +113 -0
- package/src/scheduler.js +90 -0
- package/src/task.js +189 -0
- package/src/types.js +87 -0
- package/src/workflow.js +60 -0
package/src/task.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task.js
|
|
3
|
+
*
|
|
4
|
+
* Defines the Task entity (spec sections: "Task Manager System",
|
|
5
|
+
* "Task Types", "Task State Management") and createStep(), which
|
|
6
|
+
* builds the Step objects a workflow is made of.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally a data class plus state-transition guarding — no
|
|
9
|
+
* execution, scheduling, or persistence logic lives here. That's
|
|
10
|
+
* executor.js, scheduler.js, and manager.js.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import { TaskError } from "./errors.js";
|
|
15
|
+
import { TaskType, TaskState, isValidTaskType, isValidTaskState, canTransition } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a reasonably unique id. Not cryptographically
|
|
19
|
+
* meaningful — just needs to be unique within this ZAYRA instance's
|
|
20
|
+
* task engine.
|
|
21
|
+
* @param {string} prefix
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function generateId(prefix) {
|
|
25
|
+
return `${prefix}_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A single step within a task's workflow.
|
|
30
|
+
* @typedef {object} Step
|
|
31
|
+
* @property {string} id
|
|
32
|
+
* @property {string} name
|
|
33
|
+
* @property {"pending"|"running"|"completed"|"failed"|"skipped"} status
|
|
34
|
+
* @property {any} action - Arbitrary step definition (e.g. a tool call spec). Task engine doesn't interpret this itself — the injected step runner passed to executor.js does.
|
|
35
|
+
* @property {any} result
|
|
36
|
+
* @property {{ name: string, code: string, message: string }|null} error
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds a Step from either a plain step name or a full step
|
|
41
|
+
* definition object, per spec's Workflow System example (a task's
|
|
42
|
+
* steps can be given as simple strings: "Search information",
|
|
43
|
+
* "Analyze results", "Generate summary").
|
|
44
|
+
*
|
|
45
|
+
* @param {string|object} step
|
|
46
|
+
* @returns {Step}
|
|
47
|
+
* @throws {TaskError} If the step has no usable name.
|
|
48
|
+
*/
|
|
49
|
+
export function createStep(step) {
|
|
50
|
+
const input = typeof step === "string" ? { name: step } : step ?? {};
|
|
51
|
+
|
|
52
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
53
|
+
throw new TaskError('Step requires a non-empty string "name".');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
id: input.id ?? generateId("step"),
|
|
58
|
+
name: input.name,
|
|
59
|
+
status: input.status ?? "pending",
|
|
60
|
+
action: input.action ?? null,
|
|
61
|
+
result: input.result ?? null,
|
|
62
|
+
error: input.error ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A unit of work ZAYRA's task engine plans, executes, and tracks.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const task = new Task({
|
|
71
|
+
* name: "Research topic",
|
|
72
|
+
* type: TaskType.MULTI_STEP,
|
|
73
|
+
* steps: ["Search information", "Analyze results", "Generate summary"],
|
|
74
|
+
* });
|
|
75
|
+
*/
|
|
76
|
+
export class Task {
|
|
77
|
+
/**
|
|
78
|
+
* @param {object} input
|
|
79
|
+
* @param {string} input.name - Required, non-empty.
|
|
80
|
+
* @param {string} [input.type=TaskType.SIMPLE] - One of TaskType's values.
|
|
81
|
+
* @param {string} [input.state=TaskState.CREATED] - One of TaskState's values.
|
|
82
|
+
* @param {(string|object)[]} [input.steps] - Step names or step definitions. Empty for a simple, single-action task.
|
|
83
|
+
* @param {object} [input.metadata] - Arbitrary extra data (e.g. required permissions, source, priority).
|
|
84
|
+
* @param {string} [input.id] - Task id. Auto-generated if omitted.
|
|
85
|
+
* @throws {TaskError} If required fields are missing or invalid.
|
|
86
|
+
*/
|
|
87
|
+
constructor(input = {}) {
|
|
88
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
89
|
+
throw new TaskError('Task requires a non-empty string "name".');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const type = input.type ?? TaskType.SIMPLE;
|
|
93
|
+
if (!isValidTaskType(type)) {
|
|
94
|
+
throw new TaskError(`Invalid task type: "${type}".`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const state = input.state ?? TaskState.CREATED;
|
|
98
|
+
if (!isValidTaskState(state)) {
|
|
99
|
+
throw new TaskError(`Invalid task state: "${state}".`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
103
|
+
|
|
104
|
+
this.id = input.id ?? generateId("task");
|
|
105
|
+
this.name = input.name;
|
|
106
|
+
this.type = type;
|
|
107
|
+
this.state = state;
|
|
108
|
+
this.steps = Array.isArray(input.steps)
|
|
109
|
+
? input.steps.map((step) => (step && typeof step === "object" && step.id ? step : createStep(step)))
|
|
110
|
+
: [];
|
|
111
|
+
this.currentStepIndex = input.currentStepIndex ?? 0;
|
|
112
|
+
this.result = input.result ?? null;
|
|
113
|
+
this.error = input.error ?? null;
|
|
114
|
+
this.metadata = input.metadata ?? {};
|
|
115
|
+
this.createdAt = createdAt;
|
|
116
|
+
this.updatedAt = input.updatedAt ?? createdAt;
|
|
117
|
+
this.startedAt = input.startedAt ?? null;
|
|
118
|
+
this.completedAt = input.completedAt ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @returns {boolean} Whether this task has a multi-step workflow attached. */
|
|
122
|
+
get hasWorkflow() {
|
|
123
|
+
return this.steps.length > 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @returns {Step|null} */
|
|
127
|
+
get currentStep() {
|
|
128
|
+
return this.steps[this.currentStepIndex] ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Moves the task to a new state, enforcing the allowed transitions
|
|
133
|
+
* defined in types.js. Stamps startedAt/completedAt as appropriate.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} newState
|
|
136
|
+
* @throws {TaskError} If the transition isn't allowed from the task's current state.
|
|
137
|
+
*/
|
|
138
|
+
transitionTo(newState) {
|
|
139
|
+
if (!canTransition(this.state, newState)) {
|
|
140
|
+
throw new TaskError(`Cannot transition task "${this.id}" from "${this.state}" to "${newState}".`, {
|
|
141
|
+
taskId: this.id,
|
|
142
|
+
code: "INVALID_TRANSITION",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.state = newState;
|
|
147
|
+
this.updatedAt = new Date().toISOString();
|
|
148
|
+
|
|
149
|
+
if (newState === TaskState.RUNNING && !this.startedAt) {
|
|
150
|
+
this.startedAt = this.updatedAt;
|
|
151
|
+
}
|
|
152
|
+
if ([TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED].includes(newState)) {
|
|
153
|
+
this.completedAt = this.updatedAt;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Serializes this task to a plain JSON-safe object.
|
|
159
|
+
* @returns {object}
|
|
160
|
+
*/
|
|
161
|
+
toJSON() {
|
|
162
|
+
return {
|
|
163
|
+
id: this.id,
|
|
164
|
+
name: this.name,
|
|
165
|
+
type: this.type,
|
|
166
|
+
state: this.state,
|
|
167
|
+
steps: this.steps,
|
|
168
|
+
currentStepIndex: this.currentStepIndex,
|
|
169
|
+
result: this.result,
|
|
170
|
+
error: this.error,
|
|
171
|
+
metadata: this.metadata,
|
|
172
|
+
createdAt: this.createdAt,
|
|
173
|
+
updatedAt: this.updatedAt,
|
|
174
|
+
startedAt: this.startedAt,
|
|
175
|
+
completedAt: this.completedAt,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reconstructs a Task instance from a plain object.
|
|
181
|
+
* @param {object} data
|
|
182
|
+
* @returns {Task}
|
|
183
|
+
*/
|
|
184
|
+
static fromJSON(data) {
|
|
185
|
+
return new Task(data);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default Task;
|
package/src/types.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.js
|
|
3
|
+
*
|
|
4
|
+
* Task Types + Task State Management (spec sections: "Task Types",
|
|
5
|
+
* "Task State Management"). Plain enums and validators — no
|
|
6
|
+
* behavior lives here, just the vocabulary every other file in this
|
|
7
|
+
* package shares.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What kind of task this is, per spec's "Task Types" section.
|
|
12
|
+
* @readonly
|
|
13
|
+
* @enum {string}
|
|
14
|
+
*/
|
|
15
|
+
export const TaskType = Object.freeze({
|
|
16
|
+
SIMPLE: "simple",
|
|
17
|
+
MULTI_STEP: "multi_step",
|
|
18
|
+
SCHEDULED: "scheduled",
|
|
19
|
+
AUTOMATION: "automation",
|
|
20
|
+
AI_GENERATED: "ai_generated",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Task lifecycle state, per spec's "Task State Management" section.
|
|
25
|
+
*
|
|
26
|
+
* Note: pauseTask()/resumeTask() move a task between RUNNING and
|
|
27
|
+
* WAITING. The spec's state list has no separate "paused" state, so
|
|
28
|
+
* WAITING doubles as "paused, waiting to resume" — this keeps the
|
|
29
|
+
* state machine exactly matching what the spec asked for instead of
|
|
30
|
+
* inventing an extra state.
|
|
31
|
+
*
|
|
32
|
+
* @readonly
|
|
33
|
+
* @enum {string}
|
|
34
|
+
*/
|
|
35
|
+
export const TaskState = Object.freeze({
|
|
36
|
+
CREATED: "created",
|
|
37
|
+
RUNNING: "running",
|
|
38
|
+
WAITING: "waiting",
|
|
39
|
+
COMPLETED: "completed",
|
|
40
|
+
FAILED: "failed",
|
|
41
|
+
CANCELLED: "cancelled",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** @param {string} type @returns {boolean} */
|
|
45
|
+
export function isValidTaskType(type) {
|
|
46
|
+
return Object.values(TaskType).includes(type);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} state @returns {boolean} */
|
|
50
|
+
export function isValidTaskState(state) {
|
|
51
|
+
return Object.values(TaskState).includes(state);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Allowed state transitions. Anything not listed here is rejected by
|
|
56
|
+
* Task#transitionTo() (see task.js) — so an already-completed task
|
|
57
|
+
* can't be restarted, a cancelled task can't be resumed, a failed
|
|
58
|
+
* task can't be resumed, etc.
|
|
59
|
+
*
|
|
60
|
+
* @type {Record<string, string[]>}
|
|
61
|
+
*/
|
|
62
|
+
export const TASK_STATE_TRANSITIONS = Object.freeze({
|
|
63
|
+
[TaskState.CREATED]: [TaskState.RUNNING, TaskState.CANCELLED],
|
|
64
|
+
[TaskState.RUNNING]: [TaskState.WAITING, TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED],
|
|
65
|
+
[TaskState.WAITING]: [TaskState.RUNNING, TaskState.CANCELLED],
|
|
66
|
+
[TaskState.COMPLETED]: [],
|
|
67
|
+
[TaskState.FAILED]: [],
|
|
68
|
+
[TaskState.CANCELLED]: [],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} from
|
|
73
|
+
* @param {string} to
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function canTransition(from, to) {
|
|
77
|
+
return Array.isArray(TASK_STATE_TRANSITIONS[from]) && TASK_STATE_TRANSITIONS[from].includes(to);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default {
|
|
81
|
+
TaskType,
|
|
82
|
+
TaskState,
|
|
83
|
+
isValidTaskType,
|
|
84
|
+
isValidTaskState,
|
|
85
|
+
TASK_STATE_TRANSITIONS,
|
|
86
|
+
canTransition,
|
|
87
|
+
};
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow.js
|
|
3
|
+
*
|
|
4
|
+
* Workflow System (spec section: "Workflow System"). A workflow is
|
|
5
|
+
* an ordered list of step definitions attached to a task — this
|
|
6
|
+
* module validates and builds that shape, matching the spec's
|
|
7
|
+
* example:
|
|
8
|
+
*
|
|
9
|
+
* Task: Research topic
|
|
10
|
+
* Steps: 1. Search information 2. Analyze results 3. Generate summary
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { WorkflowError } from "./errors.js";
|
|
14
|
+
import { createStep } from "./task.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates and normalizes a list of step definitions into Step
|
|
18
|
+
* objects, ready to attach to a Task (directly, or via
|
|
19
|
+
* TaskManager.createWorkflow()).
|
|
20
|
+
*
|
|
21
|
+
* @param {(string|object)[]} steps
|
|
22
|
+
* @returns {import("./task.js").Step[]}
|
|
23
|
+
* @throws {WorkflowError} If steps isn't a non-empty array, or a step definition is invalid.
|
|
24
|
+
*/
|
|
25
|
+
export function defineWorkflow(steps) {
|
|
26
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
27
|
+
throw new WorkflowError("A workflow requires a non-empty array of steps.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return steps.map((step) => createStep(step));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new WorkflowError(`Invalid workflow step: ${err.message}`, { cause: err });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Computes a workflow's progress from its current steps.
|
|
39
|
+
*
|
|
40
|
+
* @param {import("./task.js").Step[]} steps
|
|
41
|
+
* @returns {{ total: number, completed: number, failed: number, remaining: number, percent: number }}
|
|
42
|
+
*/
|
|
43
|
+
export function workflowProgress(steps) {
|
|
44
|
+
const total = steps.length;
|
|
45
|
+
const completed = steps.filter((step) => step.status === "completed").length;
|
|
46
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
total,
|
|
50
|
+
completed,
|
|
51
|
+
failed,
|
|
52
|
+
remaining: total - completed - failed,
|
|
53
|
+
percent: total === 0 ? 0 : Math.round((completed / total) * 100),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
defineWorkflow,
|
|
59
|
+
workflowProgress,
|
|
60
|
+
};
|