@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 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 { }