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/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# zayra-task-engine
|
|
2
|
+
|
|
3
|
+
The intelligent workflow execution system of **ZAYRA AI**. It manages tasks, multi-step workflows, automations, and their execution states — planning and running the work ZAYRA's AI decides to do, with permission checks in front of every run.
|
|
4
|
+
|
|
5
|
+
This is version `0.0.1`. It contains **no frontend/UI code, no hardcoded workflows, and no AI provider logic** — only task planning and execution flow.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install zayra-task-engine
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { TaskManager } from "zayra-task-engine";
|
|
17
|
+
|
|
18
|
+
const tasks = new TaskManager();
|
|
19
|
+
|
|
20
|
+
const task = tasks.createTask({ name: "Send weekly report" });
|
|
21
|
+
await tasks.startTask(task.id);
|
|
22
|
+
await tasks.completeTask(task.id, { sent: true });
|
|
23
|
+
|
|
24
|
+
task.state; // "completed"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Workflows (multi-step tasks)
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const task = tasks.createWorkflow("Research topic", [
|
|
31
|
+
"Search information",
|
|
32
|
+
"Analyze results",
|
|
33
|
+
"Generate summary",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
await tasks.startTask(task.id); // runs each step in order automatically
|
|
37
|
+
|
|
38
|
+
tasks.getProgress(task.id);
|
|
39
|
+
// { total: 3, completed: 3, failed: 0, remaining: 0, percent: 100 }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Steps run themselves through an injected runner (see **Tool integration** below) — `startTask()` walks the list, stops the whole task at the first failed step, and completes it automatically once every step succeeds.
|
|
43
|
+
|
|
44
|
+
## Task manager API
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
tasks.createTask({ name, type?, steps?, metadata? }); // register, doesn't start
|
|
48
|
+
await tasks.startTask(id); // permission check -> RUNNING -> runs steps if any
|
|
49
|
+
tasks.pauseTask(id); // RUNNING -> WAITING
|
|
50
|
+
await tasks.resumeTask(id); // WAITING -> RUNNING, continues remaining steps
|
|
51
|
+
tasks.cancelTask(id); // -> CANCELLED, also clears any pending schedule
|
|
52
|
+
await tasks.completeTask(id, result?); // -> COMPLETED, saves to memory if configured
|
|
53
|
+
await tasks.failTask(id, error?); // -> FAILED
|
|
54
|
+
|
|
55
|
+
tasks.getTask(id);
|
|
56
|
+
tasks.listTasks({ state?: "running" });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Task types
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { TaskType } from "zayra-task-engine";
|
|
63
|
+
|
|
64
|
+
TaskType.SIMPLE; // "simple" - one action, no sub-steps
|
|
65
|
+
TaskType.MULTI_STEP; // "multi_step" - an explicit workflow
|
|
66
|
+
TaskType.SCHEDULED; // "scheduled" - meant to run via scheduleTask()
|
|
67
|
+
TaskType.AUTOMATION; // "automation" - a recurring/triggered task
|
|
68
|
+
TaskType.AI_GENERATED; // "ai_generated" - steps planned by an AI orchestrator at start time
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`type` doesn't change how a task is executed by itself — it's metadata consumers (and the AI integration below) can act on.
|
|
72
|
+
|
|
73
|
+
## Task state management
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { TaskState } from "zayra-task-engine";
|
|
77
|
+
|
|
78
|
+
TaskState.CREATED; // just made, not started
|
|
79
|
+
TaskState.RUNNING; // in progress
|
|
80
|
+
TaskState.WAITING; // paused, waiting to resume
|
|
81
|
+
TaskState.COMPLETED; // finished successfully — terminal
|
|
82
|
+
TaskState.FAILED; // a step or the task itself failed — terminal
|
|
83
|
+
TaskState.CANCELLED; // cancelled by caller — terminal
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Only specific transitions are allowed (enforced by `Task#transitionTo()`, used internally by every manager method): `created -> running|cancelled`, `running -> waiting|completed|failed|cancelled`, `waiting -> running|cancelled`. Trying to move a completed/failed/cancelled task anywhere throws `TaskError`.
|
|
87
|
+
|
|
88
|
+
## Tool integration
|
|
89
|
+
|
|
90
|
+
Tasks don't call tools themselves — `TaskManager` accepts an injected `toolRunner` function, typically backed by `zayra-tools`:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { TaskManager } from "zayra-task-engine";
|
|
94
|
+
import { ToolRegistry, executeTool } from "zayra-tools";
|
|
95
|
+
|
|
96
|
+
const toolRegistry = new ToolRegistry();
|
|
97
|
+
// ...register tools...
|
|
98
|
+
|
|
99
|
+
const tasks = new TaskManager({
|
|
100
|
+
toolRunner: async (step, { task }) => {
|
|
101
|
+
const tool = toolRegistry.getTool(step.action.toolName);
|
|
102
|
+
const result = await executeTool(tool, step.action.input);
|
|
103
|
+
if (!result.success) throw new Error(result.error.message);
|
|
104
|
+
return result.data;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Flow, per spec: **Task -> Tool selection -> Execution -> Result**. `zayra-task-engine` never imports `zayra-tools` directly (see **Design note** below) — `step.action` is whatever shape your `toolRunner` expects.
|
|
110
|
+
|
|
111
|
+
## AI integration
|
|
112
|
+
|
|
113
|
+
An AI orchestrator can create tasks, plan their steps, and let the engine execute and monitor them:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
const tasks = new TaskManager({
|
|
117
|
+
aiOrchestrator: {
|
|
118
|
+
async planSteps(task) {
|
|
119
|
+
// ask an AI model what steps this task needs, return step names/definitions
|
|
120
|
+
return ["Search flights", "Compare prices", "Book cheapest option"];
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
toolRunner /* ...as above... */,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const task = tasks.createTask({ name: "Book a flight to Tokyo", type: "ai_generated" });
|
|
127
|
+
await tasks.startTask(task.id); // planSteps() runs first since the task has no steps yet, then they execute
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`aiOrchestrator` matches the shape a future `zayra-ai-orchestrator` package would provide — inject any object with a `planSteps(task)` method today.
|
|
131
|
+
|
|
132
|
+
## Memory integration
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { MemoryManager } from "zayra-memory";
|
|
136
|
+
|
|
137
|
+
const memory = new MemoryManager();
|
|
138
|
+
const tasks = new TaskManager({
|
|
139
|
+
memoryAdapter: { save: (entry) => memory.save(entry) },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await tasks.completeTask(task.id, { summary: "Report sent to 12 recipients" });
|
|
143
|
+
// memory.save() was called with a summary of the completed task
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Security rules
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { TaskPermissionManager } from "zayra-task-engine";
|
|
150
|
+
|
|
151
|
+
const permissions = new TaskPermissionManager();
|
|
152
|
+
permissions.blockTaskType("automation"); // block a whole task type
|
|
153
|
+
permissions.grantSystemPermission("network:external");
|
|
154
|
+
permissions.grantUserPermission("network:external"); // both required to actually grant it
|
|
155
|
+
|
|
156
|
+
const tasks = new TaskManager({ permissions });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`startTask()` always runs the permission check first — a task whose type is blocked, or that declares a `metadata.permissions` requirement not granted by **both** the system and the user, throws `TaskError` and never runs. This is the same allow-if-both-agree model `zayra-tools`' `PermissionManager` uses, so a shared permission set can back both packages.
|
|
160
|
+
|
|
161
|
+
## Scheduling support
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
tasks.scheduleTask(task.id, { delayMs: 60_000 }); // auto-starts in 1 minute
|
|
165
|
+
tasks.scheduleTask(task.id, { at: "2026-08-01T09:00:00Z" });
|
|
166
|
+
|
|
167
|
+
tasks.unscheduleTask(task.id); // cancel without cancelling the task itself
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A minimal timer-based scheduler in `0.0.1` — a placeholder that keeps `TaskScheduler`'s shape stable for a future cron-style/recurring version.
|
|
171
|
+
|
|
172
|
+
## Events
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { TaskEvent } from "zayra-task-engine";
|
|
176
|
+
|
|
177
|
+
tasks.events.on(TaskEvent.CREATED, ({ task }) => console.log(`${task.name} created`));
|
|
178
|
+
tasks.events.on(TaskEvent.STARTED, ({ task }) => console.log(`${task.name} started`));
|
|
179
|
+
tasks.events.on(TaskEvent.COMPLETED, ({ task }) => console.log(`${task.name} completed`));
|
|
180
|
+
tasks.events.on(TaskEvent.FAILED, ({ task }) => console.log(`${task.name} failed`));
|
|
181
|
+
tasks.events.on(TaskEvent.PAUSED, ({ task }) => {});
|
|
182
|
+
tasks.events.on(TaskEvent.RESUMED, ({ task }) => {});
|
|
183
|
+
tasks.events.on(TaskEvent.CANCELLED, ({ task }) => {});
|
|
184
|
+
tasks.events.on(TaskEvent.STEP_STARTED, ({ task, step }) => {});
|
|
185
|
+
tasks.events.on(TaskEvent.STEP_COMPLETED, ({ task, step }) => {});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A small built-in event bus for now — a drop-in placeholder for a future `zayra-events` package, matching the same `on()`/`off()`/`emit()` shape `zayra-plugins` already uses, so this code won't need to change when that lands.
|
|
189
|
+
|
|
190
|
+
## Errors
|
|
191
|
+
|
|
192
|
+
- `TaskError` — base error class; carries `err.taskId`
|
|
193
|
+
- `ExecutionError` — a step failed, timed out, or had no runner available; carries `err.stepId`
|
|
194
|
+
- `WorkflowError` — an invalid workflow definition (empty step list, malformed step)
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import { TaskError, ExecutionError, WorkflowError } from "zayra-task-engine";
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await tasks.startTask(task.id);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err instanceof TaskError) {
|
|
203
|
+
console.error(`Task ${err.taskId} failed to start:`, err.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Design note: dependency-free by convention
|
|
209
|
+
|
|
210
|
+
Like every other `zayra-*` package, `zayra-task-engine` ships with **zero npm dependencies**, including on its own siblings (`zayra-tools`, `zayra-memory`, `zayra-ai-orchestrator`, `zayra-events`). All of that integration happens through the injected adapters shown above (`toolRunner`, `memoryAdapter`, `aiOrchestrator`, `permissions`) rather than imports. This keeps each package usable standalone, testable without the rest of ZAYRA installed, and free to swap implementations later without touching this package's internals.
|
|
211
|
+
|
|
212
|
+
## What this package deliberately does NOT do
|
|
213
|
+
|
|
214
|
+
Per its design rules:
|
|
215
|
+
|
|
216
|
+
- No frontend/UI code
|
|
217
|
+
- No hardcoded workflows — every workflow is built at runtime from the steps you (or an AI orchestrator) provide
|
|
218
|
+
- No AI provider logic — model calls belong to `zayra-provider` / `zayra-ai-router`, not here
|
|
219
|
+
- No execution of unsafe operations, bypassing of permissions, or exposing of secrets — every `startTask()` runs the permission check first
|
|
220
|
+
|
|
221
|
+
## Project structure
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
zayra-task-engine/
|
|
225
|
+
src/
|
|
226
|
+
index.js # public entry point
|
|
227
|
+
manager.js # TaskManager — createTask/startTask/pauseTask/resumeTask/cancelTask/completeTask/failTask
|
|
228
|
+
task.js # Task entity, createStep(), state-transition guarding
|
|
229
|
+
workflow.js # defineWorkflow(), workflowProgress()
|
|
230
|
+
executor.js # executeStep() — safe, timeout-aware step execution
|
|
231
|
+
permissions.js # TaskPermissionManager — Security Rules
|
|
232
|
+
scheduler.js # TaskScheduler — timer-based scheduling support
|
|
233
|
+
events.js # EventBus + TaskEvent
|
|
234
|
+
types.js # TaskType, TaskState, allowed transitions
|
|
235
|
+
errors.js # TaskError, ExecutionError, WorkflowError
|
|
236
|
+
package.json
|
|
237
|
+
README.md
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## API reference
|
|
241
|
+
|
|
242
|
+
### `class TaskManager`
|
|
243
|
+
|
|
244
|
+
| Method | Description |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| `new TaskManager(options?)` | `options.permissions`, `options.toolRunner`, `options.memoryAdapter`, `options.aiOrchestrator`, `options.stepTimeoutMs`. |
|
|
247
|
+
| `createTask(input)` | Registers a task. Doesn't start it. |
|
|
248
|
+
| `createWorkflow(name, steps, options?)` | Convenience: creates a `MULTI_STEP` task from step names/definitions. |
|
|
249
|
+
| `startTask(id)` | Permission check, then `RUNNING`; plans steps via `aiOrchestrator` if `AI_GENERATED` with none; runs any steps. |
|
|
250
|
+
| `pauseTask(id)` / `resumeTask(id)` | `RUNNING <-> WAITING`; resuming continues remaining steps. |
|
|
251
|
+
| `cancelTask(id)` | -> `CANCELLED`; also clears any pending schedule. |
|
|
252
|
+
| `completeTask(id, result?)` | -> `COMPLETED`; saves a summary via `memoryAdapter` if configured. |
|
|
253
|
+
| `failTask(id, error?)` | -> `FAILED`. |
|
|
254
|
+
| `getTask(id)` / `listTasks(filter?)` | Access registered tasks. `filter.state` restricts by `TaskState`. |
|
|
255
|
+
| `scheduleTask(id, when)` / `unscheduleTask(id)` | `when: { delayMs? , at? }`. |
|
|
256
|
+
| `getProgress(id)` | `{ total, completed, failed, remaining, percent }`. |
|
|
257
|
+
|
|
258
|
+
### Other exports
|
|
259
|
+
|
|
260
|
+
`Task`, `createStep()`, `defineWorkflow()`, `workflowProgress()`, `executeStep()`, `TaskPermissionManager`, `TaskScheduler`, `EventBus`, `TaskEvent`, `TaskType`, `TaskState`, `isValidTaskType()`, `isValidTaskState()`, `canTransition()`, `TaskError`, `ExecutionError`, `WorkflowError`
|
|
261
|
+
|
|
262
|
+
## Compatibility
|
|
263
|
+
|
|
264
|
+
Designed to work with `zayra-core`, `zayra-events`, `zayra-tools`, `zayra-memory`, and a future `zayra-ai-orchestrator`.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zayra-task-engine",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Intelligent workflow execution system for ZAYRA AI — plans, tracks, and runs tasks, multi-step workflows, and automations, with permission checks and event support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test \"test/**/*.test.js\""
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"zayra",
|
|
22
|
+
"ai",
|
|
23
|
+
"task-engine",
|
|
24
|
+
"workflow",
|
|
25
|
+
"automation",
|
|
26
|
+
"orchestration",
|
|
27
|
+
"agent"
|
|
28
|
+
],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {}
|
|
32
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.js
|
|
3
|
+
*
|
|
4
|
+
* Custom error hierarchy for zayra-task-engine (spec section: "Error
|
|
5
|
+
* Handling"). Never fails silently — every error thrown inside this
|
|
6
|
+
* package is one of these classes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base error class for all zayra-task-engine errors.
|
|
11
|
+
*/
|
|
12
|
+
export class TaskError extends Error {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {string} [options.code]
|
|
17
|
+
* @param {string} [options.taskId]
|
|
18
|
+
* @param {unknown} [options.cause]
|
|
19
|
+
*/
|
|
20
|
+
constructor(message, options = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
|
|
23
|
+
this.name = this.constructor.name;
|
|
24
|
+
this.code = options.code ?? "TASK_ERROR";
|
|
25
|
+
this.taskId = options.taskId ?? null;
|
|
26
|
+
|
|
27
|
+
if (options.cause !== undefined) {
|
|
28
|
+
this.cause = options.cause;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
32
|
+
Error.captureStackTrace(this, this.constructor);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Thrown when a step fails to execute, times out, or has no runner
|
|
39
|
+
* available to execute it.
|
|
40
|
+
*/
|
|
41
|
+
export class ExecutionError extends TaskError {
|
|
42
|
+
constructor(message, options = {}) {
|
|
43
|
+
super(message, { code: "EXECUTION_ERROR", ...options });
|
|
44
|
+
this.stepId = options.stepId ?? null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Thrown for invalid workflow definitions — an empty or non-array
|
|
50
|
+
* step list, or a malformed individual step.
|
|
51
|
+
*/
|
|
52
|
+
export class WorkflowError extends TaskError {
|
|
53
|
+
constructor(message, options = {}) {
|
|
54
|
+
super(message, { code: "WORKFLOW_ERROR", ...options });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
TaskError,
|
|
60
|
+
ExecutionError,
|
|
61
|
+
WorkflowError,
|
|
62
|
+
};
|
package/src/events.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* events.js
|
|
3
|
+
*
|
|
4
|
+
* Event Support (spec section: "Event Support" — zayra-events).
|
|
5
|
+
*
|
|
6
|
+
* zayra-events doesn't exist yet as a separate package, so — same
|
|
7
|
+
* pattern already used by zayra-plugins — this file implements a
|
|
8
|
+
* small, dependency-free event emitter itself, matching that same
|
|
9
|
+
* on()/off()/emit() API. Swapping this out for a real zayra-events
|
|
10
|
+
* client later only means replacing this file's internals; every
|
|
11
|
+
* other file here only ever calls .on()/.off()/.emit().
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Task lifecycle event names. The spec explicitly lists
|
|
16
|
+
* task.created / task.started / task.completed / task.failed;
|
|
17
|
+
* task.paused, task.resumed, task.cancelled, and the per-step events
|
|
18
|
+
* are natural extensions of the same six task-manager operations
|
|
19
|
+
* (createTask/startTask/pauseTask/resumeTask/cancelTask/completeTask)
|
|
20
|
+
* and the workflow step loop.
|
|
21
|
+
*
|
|
22
|
+
* @readonly
|
|
23
|
+
* @enum {string}
|
|
24
|
+
*/
|
|
25
|
+
export const TaskEvent = Object.freeze({
|
|
26
|
+
CREATED: "task.created",
|
|
27
|
+
STARTED: "task.started",
|
|
28
|
+
PAUSED: "task.paused",
|
|
29
|
+
RESUMED: "task.resumed",
|
|
30
|
+
COMPLETED: "task.completed",
|
|
31
|
+
FAILED: "task.failed",
|
|
32
|
+
CANCELLED: "task.cancelled",
|
|
33
|
+
STEP_STARTED: "task.step.started",
|
|
34
|
+
STEP_COMPLETED: "task.step.completed",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Minimal synchronous event emitter.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const events = new EventBus();
|
|
42
|
+
* events.on(TaskEvent.COMPLETED, ({ task }) => console.log(`${task.name} done`));
|
|
43
|
+
* events.emit(TaskEvent.COMPLETED, { task });
|
|
44
|
+
*/
|
|
45
|
+
export class EventBus {
|
|
46
|
+
constructor() {
|
|
47
|
+
/** @type {Map<string, Set<Function>>} */
|
|
48
|
+
this._listeners = new Map();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} eventName
|
|
53
|
+
* @param {(payload: any) => void} handler
|
|
54
|
+
* @returns {() => void} An unsubscribe function.
|
|
55
|
+
*/
|
|
56
|
+
on(eventName, handler) {
|
|
57
|
+
if (typeof handler !== "function") {
|
|
58
|
+
throw new TypeError("EventBus.on() requires a function handler.");
|
|
59
|
+
}
|
|
60
|
+
if (!this._listeners.has(eventName)) {
|
|
61
|
+
this._listeners.set(eventName, new Set());
|
|
62
|
+
}
|
|
63
|
+
this._listeners.get(eventName).add(handler);
|
|
64
|
+
return () => this.off(eventName, handler);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} eventName
|
|
69
|
+
* @param {(payload: any) => void} handler
|
|
70
|
+
* @returns {() => void} An unsubscribe function.
|
|
71
|
+
*/
|
|
72
|
+
once(eventName, handler) {
|
|
73
|
+
const wrapped = (payload) => {
|
|
74
|
+
this.off(eventName, wrapped);
|
|
75
|
+
handler(payload);
|
|
76
|
+
};
|
|
77
|
+
return this.on(eventName, wrapped);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} eventName
|
|
82
|
+
* @param {(payload: any) => void} handler
|
|
83
|
+
*/
|
|
84
|
+
off(eventName, handler) {
|
|
85
|
+
this._listeners.get(eventName)?.delete(handler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Emits an event synchronously. A throwing handler is caught and
|
|
90
|
+
* logged rather than propagating, so one bad listener can't break
|
|
91
|
+
* the emit chain or crash the task lifecycle step that triggered it.
|
|
92
|
+
* @param {string} eventName
|
|
93
|
+
* @param {any} [payload]
|
|
94
|
+
*/
|
|
95
|
+
emit(eventName, payload) {
|
|
96
|
+
const handlers = this._listeners.get(eventName);
|
|
97
|
+
if (!handlers || handlers.size === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const handler of handlers) {
|
|
101
|
+
try {
|
|
102
|
+
handler(payload);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`[zayra-task-engine] Event handler for "${eventName}" threw:`, err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @param {string} eventName @returns {number} */
|
|
110
|
+
listenerCount(eventName) {
|
|
111
|
+
return this._listeners.get(eventName)?.size ?? 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {string} [eventName] */
|
|
115
|
+
removeAllListeners(eventName) {
|
|
116
|
+
if (eventName) {
|
|
117
|
+
this._listeners.delete(eventName);
|
|
118
|
+
} else {
|
|
119
|
+
this._listeners.clear();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default EventBus;
|
package/src/executor.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* executor.js
|
|
3
|
+
*
|
|
4
|
+
* Safe step execution (spec sections: "Tool Integration" — Task ->
|
|
5
|
+
* Tool selection -> Execution -> Result — and "Error Handling":
|
|
6
|
+
* failed steps, timeouts).
|
|
7
|
+
*
|
|
8
|
+
* zayra-tools already exists as a sibling package, but per this
|
|
9
|
+
* project's convention every zayra-* package ships with zero npm
|
|
10
|
+
* dependencies on its siblings (see each package.json's empty
|
|
11
|
+
* "dependencies") — integration happens through dependency injection
|
|
12
|
+
* instead. Callers pass a `runner(step, context)` function (e.g. one
|
|
13
|
+
* backed by zayra-tools' ToolRegistry + executeTool()); this module
|
|
14
|
+
* just adds the safety net around it: an optional timeout, and a
|
|
15
|
+
* standard { success, data, error } result that never throws —
|
|
16
|
+
* matching zayra-tools' own executeTool() result shape.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ExecutionError } from "./errors.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} StepResult
|
|
23
|
+
* @property {boolean} success
|
|
24
|
+
* @property {any} data
|
|
25
|
+
* @property {{ name: string, code: string, message: string }|null} error
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serializes any thrown value into the result format's `error` shape.
|
|
30
|
+
* @param {unknown} err
|
|
31
|
+
* @returns {{ name: string, code: string, message: string }}
|
|
32
|
+
*/
|
|
33
|
+
function serializeError(err) {
|
|
34
|
+
if (err instanceof Error) {
|
|
35
|
+
return {
|
|
36
|
+
name: err.name,
|
|
37
|
+
code: err.code ?? "EXECUTION_ERROR",
|
|
38
|
+
message: err.message,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { name: "Error", code: "EXECUTION_ERROR", message: String(err) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Races a promise against a timeout, rejecting with an ExecutionError
|
|
46
|
+
* if the timeout wins.
|
|
47
|
+
* @param {Promise<any>} promise
|
|
48
|
+
* @param {number} timeoutMs
|
|
49
|
+
* @param {string} stepId
|
|
50
|
+
* @returns {Promise<any>}
|
|
51
|
+
*/
|
|
52
|
+
function withTimeout(promise, timeoutMs, stepId) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
reject(new ExecutionError(`Step timed out after ${timeoutMs}ms.`, { stepId, code: "TIMEOUT" }));
|
|
56
|
+
}, timeoutMs);
|
|
57
|
+
|
|
58
|
+
promise.then(
|
|
59
|
+
(value) => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve(value);
|
|
62
|
+
},
|
|
63
|
+
(err) => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Executes a single step through an injected runner function,
|
|
73
|
+
* enforcing an optional timeout and normalizing the result. Never
|
|
74
|
+
* rejects.
|
|
75
|
+
*
|
|
76
|
+
* @param {import("./task.js").Step} step
|
|
77
|
+
* @param {(step: import("./task.js").Step, context: object) => Promise<any>} runner - Injected step runner, e.g. one that looks up a tool by `step.action` and calls zayra-tools' executeTool().
|
|
78
|
+
* @param {object} [context] - Passed through to the runner (e.g. `{ task }`).
|
|
79
|
+
* @param {number} [timeoutMs] - Optional timeout in ms. Omit/0 for no timeout.
|
|
80
|
+
* @returns {Promise<StepResult>}
|
|
81
|
+
*/
|
|
82
|
+
export async function executeStep(step, runner, context = {}, timeoutMs) {
|
|
83
|
+
if (typeof runner !== "function") {
|
|
84
|
+
const err = new ExecutionError(
|
|
85
|
+
`No step runner was provided for step "${step.name}" (expected a function).`,
|
|
86
|
+
{ stepId: step.id }
|
|
87
|
+
);
|
|
88
|
+
return { success: false, data: null, error: serializeError(err) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const data = timeoutMs
|
|
93
|
+
? await withTimeout(runner(step, context), timeoutMs, step.id)
|
|
94
|
+
: await runner(step, context);
|
|
95
|
+
return { success: true, data: data === undefined ? null : data, error: null };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const wrapped =
|
|
98
|
+
err instanceof ExecutionError
|
|
99
|
+
? err
|
|
100
|
+
: new ExecutionError(`Step "${step.name}" failed: ${err.message}`, {
|
|
101
|
+
stepId: step.id,
|
|
102
|
+
cause: err,
|
|
103
|
+
});
|
|
104
|
+
return { success: false, data: null, error: serializeError(wrapped) };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default {
|
|
109
|
+
executeStep,
|
|
110
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js
|
|
3
|
+
*
|
|
4
|
+
* Public entry point for the zayra-task-engine package.
|
|
5
|
+
*
|
|
6
|
+
* Anything a consumer needs should be exported from here — they
|
|
7
|
+
* should never need to import from "zayra-task-engine/src/...". This
|
|
8
|
+
* keeps the internal file layout free to change without breaking
|
|
9
|
+
* zayra-core, zayra-ai-orchestrator, zayra-tools, or zayra-memory
|
|
10
|
+
* integrations built against this package.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { TaskManager } from "./manager.js";
|
|
14
|
+
export { Task, createStep } from "./task.js";
|
|
15
|
+
export { defineWorkflow, workflowProgress } from "./workflow.js";
|
|
16
|
+
export { executeStep } from "./executor.js";
|
|
17
|
+
export { TaskPermissionManager } from "./permissions.js";
|
|
18
|
+
export { TaskScheduler } from "./scheduler.js";
|
|
19
|
+
export { EventBus, TaskEvent } from "./events.js";
|
|
20
|
+
export { TaskType, TaskState, isValidTaskType, isValidTaskState, canTransition } from "./types.js";
|
|
21
|
+
export { TaskError, ExecutionError, WorkflowError } from "./errors.js";
|
|
22
|
+
|
|
23
|
+
export { TaskManager as default } from "./manager.js";
|