@orkestrel/workflow 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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/src/browser/index.d.ts +279 -0
- package/dist/src/browser/index.js +399 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +2805 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +3179 -0
- package/dist/src/core/index.d.ts +3179 -0
- package/dist/src/core/index.js +2734 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/server/index.cjs +129 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.cts +93 -0
- package/dist/src/server/index.d.ts +93 -0
- package/dist/src/server/index.js +127 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +111 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orkestrel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @orkestrel/workflow
|
|
2
|
+
|
|
3
|
+
A typed workflow engine for the `@orkestrel` line — a serializable
|
|
4
|
+
`Workflow → Phase → Task` tree that a UI or an LLM authors as pure JSON, and
|
|
5
|
+
a thin `WorkflowRunner` executes by COMPOSING the shipped substrate (a
|
|
6
|
+
per-phase `Runner`, `Abort`, `Timeout`, `Budget`, and a cooperative
|
|
7
|
+
cross-environment `Scheduler`) rather than re-implementing its own
|
|
8
|
+
concurrency / retry / abort machinery.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @orkestrel/workflow
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Core is cross-environment ESM; `./browser` adds browser-native cooperative
|
|
19
|
+
scheduler backends (`requestAnimationFrame` / `requestIdleCallback` /
|
|
20
|
+
Prioritized Task Scheduling), `./server` adds the Node-native
|
|
21
|
+
`setImmediate` scheduler backend
|
|
22
|
+
|
|
23
|
+
## Status
|
|
24
|
+
|
|
25
|
+
Pre-release (`0.0.1`): the definition contract, the live entity tree, the
|
|
26
|
+
thin runner (with the `function` / `tool` / `agent` task forms and the
|
|
27
|
+
depth/cycle-bounded agent-native recursion), the durable `WorkflowStore`
|
|
28
|
+
(in-memory + driver-pluggable), and the cooperative `Scheduler` (the
|
|
29
|
+
cross-environment default plus the browser and Node environment backends)
|
|
30
|
+
are all implemented and tested, but the public API is still unstable and
|
|
31
|
+
may change without notice. See [guides/src/workflow.md](./guides/src/workflow.md)
|
|
32
|
+
for the full documented surface.
|
|
33
|
+
|
|
34
|
+
## Package
|
|
35
|
+
|
|
36
|
+
Published as three environment-scoped entry points per the `exports` field
|
|
37
|
+
in `package.json`: `.` (the shared, environment-agnostic core — the
|
|
38
|
+
definition/entity/runner surface plus the cross-environment `Scheduler`
|
|
39
|
+
default), `./browser` (adds the browser-native scheduler backends), and
|
|
40
|
+
`./server` (adds the Node-native scheduler backend). Core ships dual
|
|
41
|
+
ESM+CJS builds; `./browser` is ESM-only.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { SchedulerInterface } from '../core/index.js';
|
|
2
|
+
import { SchedulerOptions } from '../core/index.js';
|
|
3
|
+
import { SchedulerPriority } from '../core/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The browser {@link SchedulerInterface} — the browser-native cooperative-yield backend
|
|
7
|
+
* built on the Prioritized Task Scheduling API (`scheduler.postTask`), falling back to a
|
|
8
|
+
* zero-delay macrotask where it is absent.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* - **`yield` prefers `scheduler.postTask`, honouring priority.** When `globalThis`
|
|
12
|
+
* exposes a `scheduler` with a `postTask` method, `yield()` posts a task at the mapped
|
|
13
|
+
* priority (`user` → `'user-blocking'`, `normal` → `'user-visible'`, `background` →
|
|
14
|
+
* `'background'`), so the host genuinely regains control and the urgency hint is
|
|
15
|
+
* honoured. The capability is feature-detected through guards (`isRecord` / `isFunction`),
|
|
16
|
+
* never an `as` (AGENTS §14). Where the API is absent (Firefox today, older engines),
|
|
17
|
+
* it **falls back** to a `setTimeout(0)` macrotask — still a real host-turn, just
|
|
18
|
+
* without priority. `delay(ms)` is always a real `setTimeout`.
|
|
19
|
+
* - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with `signal.reason`
|
|
20
|
+
* exactly — the value the caller passed, never wrapped or replaced. The discipline
|
|
21
|
+
* mirrors the cross-environment default's `#sleep`: an already-aborted signal rejects
|
|
22
|
+
* immediately WITHOUT scheduling; otherwise the host-turn is scheduled and a
|
|
23
|
+
* `{ once: true }` abort listener attached, and the two settle paths are mutually
|
|
24
|
+
* exclusive — the turn path removes the listener before resolving, and the abort path
|
|
25
|
+
* cancels the scheduled turn before rejecting. The promise settles exactly once, with no
|
|
26
|
+
* leaked task/timer and no leaked listener. The caller's `signal` is NOT handed to
|
|
27
|
+
* `postTask` (whose own abort would reject with a platform `AbortError`, not the
|
|
28
|
+
* caller's `reason`); instead an internal controller cancels the posted task while this
|
|
29
|
+
* scheduler rejects with the verbatim `signal.reason`.
|
|
30
|
+
* - **Event-free.** A pure functional primitive — no Emitter, no events.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { createAbort } from '../core/index.js'
|
|
35
|
+
* import { BrowserScheduler } from '@src/browser'
|
|
36
|
+
*
|
|
37
|
+
* const abort = createAbort()
|
|
38
|
+
* const scheduler = new BrowserScheduler()
|
|
39
|
+
* while (!abort.signal.aborted) {
|
|
40
|
+
* doSomeWork()
|
|
41
|
+
* await scheduler.yield({ priority: 'background', signal: abort.signal })
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare class BrowserScheduler implements SchedulerInterface {
|
|
46
|
+
#private;
|
|
47
|
+
/**
|
|
48
|
+
* Yield control to the host via `scheduler.postTask` at the given priority (or a
|
|
49
|
+
* `setTimeout(0)` macrotask where the API is absent), then resume; abort rejects with
|
|
50
|
+
* `signal.reason`.
|
|
51
|
+
*/
|
|
52
|
+
yield(options?: SchedulerOptions): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
|
|
55
|
+
* `signal.reason`.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* `ms` should be a non-negative finite number. The primitive does no validation: it
|
|
59
|
+
* passes `ms` straight to the host `setTimeout`, which clamps a negative value or
|
|
60
|
+
* `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
|
|
61
|
+
* throwing.
|
|
62
|
+
*/
|
|
63
|
+
delay(ms: number, options?: SchedulerOptions): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create the browser-native cooperative-yield {@link SchedulerInterface} — `yield()` uses
|
|
68
|
+
* the Prioritized Task Scheduling API (`scheduler.postTask`) at the requested priority
|
|
69
|
+
* when present, falling back to a `setTimeout(0)` macrotask; `delay(ms)` is a real
|
|
70
|
+
* `setTimeout`.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* The default browser scheduler: it honours `options.priority` (`user` /
|
|
74
|
+
* `normal` / `background`) when `scheduler.postTask` is available and degrades to a plain
|
|
75
|
+
* macrotask elsewhere. Both methods are abort-aware: pass `options.signal` and a pending
|
|
76
|
+
* yield/delay rejects with the signal's `reason` verbatim, with full task/timer/listener
|
|
77
|
+
* cleanup. Prefer {@link createFrameScheduler} for paint-aligned work or
|
|
78
|
+
* {@link createIdleScheduler} for idle-time background work.
|
|
79
|
+
*
|
|
80
|
+
* @returns A {@link SchedulerInterface} backed by `scheduler.postTask` (or a macrotask)
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { createBrowserScheduler } from '@src/browser'
|
|
85
|
+
*
|
|
86
|
+
* const scheduler = createBrowserScheduler()
|
|
87
|
+
* await scheduler.yield({ priority: 'background' })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function createBrowserScheduler(): SchedulerInterface;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create the frame-aligned cooperative-yield {@link SchedulerInterface} — `yield()` resumes
|
|
94
|
+
* just before the next paint via `requestAnimationFrame`; `delay(ms)` is a real
|
|
95
|
+
* `setTimeout`.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Use it for work that should batch per render frame (animation, incremental DOM updates)
|
|
99
|
+
* and naturally pause while the tab is hidden. `yield` is abort-aware: pass `options.signal`
|
|
100
|
+
* and a pending yield rejects with the signal's `reason` verbatim, cancelling the pending
|
|
101
|
+
* frame request. `options.priority` is accepted but a no-op — a frame callback has no
|
|
102
|
+
* priority dimension.
|
|
103
|
+
*
|
|
104
|
+
* @returns A {@link SchedulerInterface} backed by `requestAnimationFrame`
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* import { createFrameScheduler } from '@src/browser'
|
|
109
|
+
*
|
|
110
|
+
* const scheduler = createFrameScheduler()
|
|
111
|
+
* await scheduler.yield() // resumes before the next paint
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export declare function createFrameScheduler(): SchedulerInterface;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create the idle-time cooperative-yield {@link SchedulerInterface} — `yield()` resumes when
|
|
118
|
+
* the host is idle via `requestIdleCallback` when present, falling back to a `setTimeout(0)`
|
|
119
|
+
* macrotask; `delay(ms)` is a real `setTimeout`.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* Use it for low-priority background work that must not contend with rendering or input.
|
|
123
|
+
* Where `requestIdleCallback` is absent (Safari today) it degrades to a plain macrotask.
|
|
124
|
+
* `yield` is abort-aware: pass `options.signal` and a pending yield rejects with the
|
|
125
|
+
* signal's `reason` verbatim, cancelling the pending idle callback. `options.priority` is
|
|
126
|
+
* accepted but a no-op — idle scheduling has no priority dimension.
|
|
127
|
+
*
|
|
128
|
+
* @returns A {@link SchedulerInterface} backed by `requestIdleCallback` (or a macrotask)
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* import { createIdleScheduler } from '@src/browser'
|
|
133
|
+
*
|
|
134
|
+
* const scheduler = createIdleScheduler()
|
|
135
|
+
* await scheduler.yield() // resumes when the host is idle
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare function createIdleScheduler(): SchedulerInterface;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The frame-aligned {@link SchedulerInterface} — a browser cooperative-yield backend
|
|
142
|
+
* whose `yield` resumes just before the next paint via `requestAnimationFrame`.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* - **`yield` resumes before the next paint.** `yield()` waits on `requestAnimationFrame`,
|
|
146
|
+
* so the resumption is aligned to the browser's render loop — ideal for work that
|
|
147
|
+
* should batch per frame (animation, incremental DOM updates) and pause while the tab
|
|
148
|
+
* is hidden (the host throttles rAF). `delay(ms)` is a real `setTimeout`, unaligned to
|
|
149
|
+
* frames. `options.priority` is accepted for contract compliance but a no-op — a frame
|
|
150
|
+
* callback has no priority dimension.
|
|
151
|
+
* - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with
|
|
152
|
+
* `signal.reason` exactly. The discipline mirrors the cross-environment default's
|
|
153
|
+
* `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling a frame;
|
|
154
|
+
* otherwise the frame is requested and a `{ once: true }` abort listener attached, and
|
|
155
|
+
* the two settle paths are mutually exclusive — the frame path removes the listener
|
|
156
|
+
* before resolving, and the abort path `cancelAnimationFrame`s the pending handle before
|
|
157
|
+
* rejecting. The promise settles exactly once, with no leaked frame request and no
|
|
158
|
+
* leaked listener.
|
|
159
|
+
* - **Event-free.** A pure functional primitive — no Emitter, no events.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { createAbort } from '../core/index.js'
|
|
164
|
+
* import { FrameScheduler } from '@src/browser'
|
|
165
|
+
*
|
|
166
|
+
* const abort = createAbort()
|
|
167
|
+
* const scheduler = new FrameScheduler()
|
|
168
|
+
* while (!abort.signal.aborted) {
|
|
169
|
+
* renderOneFrameOfWork()
|
|
170
|
+
* await scheduler.yield({ signal: abort.signal }) // resume before the next paint
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export declare class FrameScheduler implements SchedulerInterface {
|
|
175
|
+
#private;
|
|
176
|
+
/**
|
|
177
|
+
* Yield control to the host until just before the next paint via
|
|
178
|
+
* `requestAnimationFrame`, then resume; abort rejects with `signal.reason`.
|
|
179
|
+
*/
|
|
180
|
+
yield(options?: SchedulerOptions): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
|
|
183
|
+
* `signal.reason`.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* `ms` should be a non-negative finite number. The primitive does no validation: it
|
|
187
|
+
* passes `ms` straight to the host `setTimeout`, which clamps a negative value or
|
|
188
|
+
* `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
|
|
189
|
+
* throwing.
|
|
190
|
+
*/
|
|
191
|
+
delay(ms: number, options?: SchedulerOptions): Promise<void>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The narrowed `requestIdleCallback` / `cancelIdleCallback` pair feature-detected off
|
|
196
|
+
* `globalThis`.
|
|
197
|
+
*
|
|
198
|
+
* @remarks
|
|
199
|
+
* A `request` taking a callback and returning a numeric handle, and a `cancel` taking that
|
|
200
|
+
* handle. {@link IdleScheduler} feature-detects the pair through a guard (`isFunction`,
|
|
201
|
+
* never an `as` — AGENTS §14) and resolves to `undefined` when the API is absent (Safari
|
|
202
|
+
* today), so `yield` falls back to a macrotask.
|
|
203
|
+
*/
|
|
204
|
+
export declare interface IdleAPI {
|
|
205
|
+
readonly request: (callback: () => void) => number;
|
|
206
|
+
readonly cancel: (handle: number) => void;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The idle-time {@link SchedulerInterface} — a browser cooperative-yield backend whose
|
|
211
|
+
* `yield` resumes when the host is idle via `requestIdleCallback`, falling back to a
|
|
212
|
+
* zero-delay macrotask where it is absent.
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* - **`yield` resumes during idle time.** When `globalThis` exposes `requestIdleCallback`,
|
|
216
|
+
* `yield()` waits on it, so the resumption happens when the browser has spare time after
|
|
217
|
+
* rendering and input — ideal for low-priority background work that must not contend with
|
|
218
|
+
* the user. The capability is feature-detected through a guard (`isFunction`), never an
|
|
219
|
+
* `as` (AGENTS §14). Where the API is absent (Safari today), it **falls back** to a
|
|
220
|
+
* `setTimeout(0)` macrotask — still a real host-turn, just not idle-gated. `delay(ms)` is
|
|
221
|
+
* always a real `setTimeout`. `options.priority` is accepted for contract compliance but a
|
|
222
|
+
* no-op — idle scheduling has no priority dimension.
|
|
223
|
+
* - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with
|
|
224
|
+
* `signal.reason` exactly. The discipline mirrors the cross-environment default's
|
|
225
|
+
* `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling; otherwise
|
|
226
|
+
* the idle callback (or fallback timer) is requested and a `{ once: true }` abort listener
|
|
227
|
+
* attached, and the two settle paths are mutually exclusive — the resume path removes the
|
|
228
|
+
* listener before resolving, and the abort path `cancelIdleCallback`s (or `clearTimeout`s)
|
|
229
|
+
* the pending handle before rejecting. The promise settles exactly once, with no leaked
|
|
230
|
+
* callback/timer and no leaked listener.
|
|
231
|
+
* - **Event-free.** A pure functional primitive — no Emitter, no events.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* import { createAbort } from '../core/index.js'
|
|
236
|
+
* import { IdleScheduler } from '@src/browser'
|
|
237
|
+
*
|
|
238
|
+
* const abort = createAbort()
|
|
239
|
+
* const scheduler = new IdleScheduler()
|
|
240
|
+
* while (!abort.signal.aborted) {
|
|
241
|
+
* doLowPriorityWork()
|
|
242
|
+
* await scheduler.yield({ signal: abort.signal }) // resume when the host is idle
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export declare class IdleScheduler implements SchedulerInterface {
|
|
247
|
+
#private;
|
|
248
|
+
/**
|
|
249
|
+
* Yield control to the host until it is idle via `requestIdleCallback` (or a
|
|
250
|
+
* `setTimeout(0)` macrotask where the API is absent), then resume; abort rejects with
|
|
251
|
+
* `signal.reason`.
|
|
252
|
+
*/
|
|
253
|
+
yield(options?: SchedulerOptions): Promise<void>;
|
|
254
|
+
/**
|
|
255
|
+
* Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
|
|
256
|
+
* `signal.reason`.
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* `ms` should be a non-negative finite number. The primitive does no validation: it
|
|
260
|
+
* passes `ms` straight to the host `setTimeout`, which clamps a negative value or
|
|
261
|
+
* `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
|
|
262
|
+
* throwing.
|
|
263
|
+
*/
|
|
264
|
+
delay(ms: number, options?: SchedulerOptions): Promise<void>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The browser-native `postTask` priority for each portable {@link SchedulerPriority} — the
|
|
269
|
+
* Prioritized Task Scheduling API's three levels.
|
|
270
|
+
*
|
|
271
|
+
* @remarks
|
|
272
|
+
* A `user` hint maps to the most urgent `'user-blocking'`, `normal` to the default
|
|
273
|
+
* `'user-visible'`, and `background` to `'background'`. {@link BrowserScheduler} reads this
|
|
274
|
+
* map to translate the caller's portable priority into the value passed to
|
|
275
|
+
* `scheduler.postTask`, so the urgency hint is honoured by the host.
|
|
276
|
+
*/
|
|
277
|
+
export declare const POST_TASK_PRIORITY: Readonly<Record<SchedulerPriority, string>>;
|
|
278
|
+
|
|
279
|
+
export { }
|