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/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
+ };
@@ -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
+ };