@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.
@@ -0,0 +1,2734 @@
1
+ import { createTool } from "@orkestrel/agent";
2
+ import { arrayShape, createContract, integerShape, isArray, isBoolean, isNumber, isRecord, isString, literalShape, objectShape, optionalShape, rawShape, schemaToParameters, stringShape, unionShape } from "@orkestrel/contract";
3
+ import { createDatabase, createMemoryDriver } from "@orkestrel/database";
4
+ import { Emitter } from "@orkestrel/emitter";
5
+ import { createAbort } from "@orkestrel/abort";
6
+ import { createTimeout } from "@orkestrel/timeout";
7
+ import { createQueue } from "@orkestrel/queue";
8
+ //#region src/core/Scheduler.ts
9
+ /**
10
+ * The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
11
+ * built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
12
+ * browser and Node.
13
+ *
14
+ * @remarks
15
+ * - **Cross-environment.** Uses ONLY `setTimeout` / `clearTimeout` — universally
16
+ * available. It deliberately avoids env-specific fast paths (`setImmediate`,
17
+ * `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
18
+ * `MessageChannel`); those belong to the environment backends, built with the
19
+ * agent loop that consumes them.
20
+ * - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
21
+ * `setTimeout(0)`, NOT `queueMicrotask`. A microtask drains before the host
22
+ * regains control, so it would not actually let pending I/O, timers, or
23
+ * rendering run — it only defers within the current task. A zero-delay timer is
24
+ * the correct cross-environment "give the host a turn".
25
+ * - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` when
26
+ * the signal aborts (the standard `AbortSignal` convention). An already-aborted
27
+ * signal rejects immediately without arming a timer. Either settle path clears
28
+ * the timer and removes the abort listener — no leaked timer, no leaked
29
+ * listener, and no double-settle.
30
+ * - **Priority is accepted but uniform.** `options.priority` is part of the
31
+ * contract, but a `setTimeout`-based default cannot act on urgency, so it treats
32
+ * every priority the same. Environment backends honour it.
33
+ * - **Event-free.** A pure functional primitive — no Emitter, no events.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const scheduler = new Scheduler()
38
+ * while (!signal.aborted) {
39
+ * doSomeWork()
40
+ * await scheduler.yield({ signal }) // let the host run between work units
41
+ * }
42
+ * ```
43
+ */
44
+ var Scheduler = class {
45
+ /**
46
+ * Yield control back to the host so other tasks (I/O, timers, rendering) can
47
+ * run, then resume — a macrotask turn via `setTimeout(0)` (NOT a microtask,
48
+ * which would resume before the host regains control).
49
+ */
50
+ yield(options) {
51
+ return this.#sleep(0, options?.signal);
52
+ }
53
+ /**
54
+ * Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
55
+ *
56
+ * @remarks
57
+ * `ms` should be a non-negative finite number. The primitive stays minimal and
58
+ * does no validation: it passes `ms` straight to the host `setTimeout`, which
59
+ * clamps a negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on
60
+ * the next host turn rather than throwing.
61
+ */
62
+ delay(ms, options) {
63
+ return this.#sleep(ms, options?.signal);
64
+ }
65
+ #sleep(ms, signal) {
66
+ if (signal?.aborted === true) return Promise.reject(signal.reason);
67
+ return new Promise((resolve, reject) => {
68
+ const onAbort = () => {
69
+ clearTimeout(handle);
70
+ reject(signal?.reason);
71
+ };
72
+ const handle = setTimeout(() => {
73
+ signal?.removeEventListener("abort", onAbort);
74
+ resolve();
75
+ }, ms);
76
+ signal?.addEventListener("abort", onAbort, { once: true });
77
+ });
78
+ }
79
+ };
80
+ //#endregion
81
+ //#region src/core/constants.ts
82
+ /** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
83
+ var DEFAULT_BAIL = false;
84
+ /**
85
+ * The three task-form mechanisms (the {@link TaskVia} discriminant), frozen.
86
+ *
87
+ * @remarks
88
+ * The runtime source of truth for the `via` axis — drive the contract's literal
89
+ * shape and any guard from this array rather than repeating the literals.
90
+ */
91
+ var TASK_VIAS = Object.freeze([
92
+ "function",
93
+ "tool",
94
+ "agent"
95
+ ]);
96
+ /**
97
+ * Every {@link TaskStatus} value, frozen — the lifecycle vocabulary of a task.
98
+ *
99
+ * @remarks
100
+ * Ordered pending → running → terminal (`completed` / `failed` / `skipped` /
101
+ * `stopped`). The source of truth for the union; compose guards / shapes from it.
102
+ */
103
+ var TASK_STATUSES = Object.freeze([
104
+ "pending",
105
+ "running",
106
+ "completed",
107
+ "failed",
108
+ "skipped",
109
+ "stopped"
110
+ ]);
111
+ /** Every {@link PhaseStatus} value, frozen — the lifecycle vocabulary of a phase. */
112
+ var PHASE_STATUSES = Object.freeze([
113
+ "pending",
114
+ "running",
115
+ "completed",
116
+ "failed",
117
+ "skipped",
118
+ "stopped"
119
+ ]);
120
+ /** Every {@link WorkflowStatus} value, frozen — the lifecycle vocabulary of a workflow. */
121
+ var WORKFLOW_STATUSES = Object.freeze([
122
+ "pending",
123
+ "running",
124
+ "completed",
125
+ "failed",
126
+ "skipped",
127
+ "stopped"
128
+ ]);
129
+ /**
130
+ * The {@link TaskStatus} values that are TERMINAL — a task in one of these will
131
+ * not transition further, frozen.
132
+ *
133
+ * @remarks
134
+ * The source of truth behind {@link import('./helpers.js').isTerminalStatus}.
135
+ * `pending` and `running` are the only non-terminal members.
136
+ */
137
+ var TERMINAL_TASK_STATUSES = Object.freeze([
138
+ "completed",
139
+ "failed",
140
+ "skipped",
141
+ "stopped"
142
+ ]);
143
+ /**
144
+ * The legal {@link TaskStatus} transition graph of the live W-b task state machine —
145
+ * each current status mapped to the statuses it may move to directly, frozen.
146
+ *
147
+ * @remarks
148
+ * The source of truth behind {@link import('./helpers.js').canTransitionTask} and the
149
+ * `TRANSITION` guard ({@link import('./errors.js').WorkflowError}). A `pending` task may
150
+ * `start` (→ `running`), `skip` (→ `skipped`), or `stop` (→ `stopped`); a `running` task
151
+ * may `complete` (→ `completed`), `fail` (→ `failed`), `skip` (→ `skipped`), or `stop`
152
+ * (→ `stopped`). Every terminal status maps to an empty list — a settled task never
153
+ * transitions again. So completing a non-`running` task, or starting a settled one, is
154
+ * rejected.
155
+ */
156
+ var TASK_TRANSITIONS = Object.freeze({
157
+ pending: [
158
+ "running",
159
+ "skipped",
160
+ "stopped"
161
+ ],
162
+ running: [
163
+ "completed",
164
+ "failed",
165
+ "skipped",
166
+ "stopped"
167
+ ],
168
+ completed: [],
169
+ failed: [],
170
+ skipped: [],
171
+ stopped: []
172
+ });
173
+ /**
174
+ * The default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
175
+ * runner applies when a {@link import('./types.js').PhaseDefinition} omits its `concurrency`
176
+ * throttle — a large cap that is effectively unbounded for any realistic phase.
177
+ *
178
+ * @remarks
179
+ * The determinism principle fixes that a phase's tasks run CONCURRENTLY; `concurrency` is
180
+ * only an optional resource throttle (max-in-flight). With none declared, the runner runs
181
+ * all of a phase's tasks at once — modelled as this large finite cap so the value flows
182
+ * straight into the substrate {@link import('./types.js').RunnerInterface}'s
183
+ * `concurrency` (which expects a positive integer) without a special unbounded branch. No
184
+ * realistic phase declares enough tasks to reach it, so it behaves as "run them all".
185
+ */
186
+ var DEFAULT_PHASE_CONCURRENCY = 1e6;
187
+ /**
188
+ * The maximum nesting depth a workflow's `agent` task may spawn into (W-c) — the
189
+ * bound the runner's depth/cycle guard enforces.
190
+ *
191
+ * @remarks
192
+ * The limit lives in ONE place. The `agent` {@link import('./types.js').TaskForm} is
193
+ * bounded by it when the {@link import('./WorkflowRunner.js').WorkflowRunner} resolves a
194
+ * subagent: an agent running at this depth can no longer author + run a nested workflow
195
+ * (that would be depth `MAX_WORKFLOW_DEPTH + 1`), so the over-deep `agent` task is
196
+ * rejected (a typed `DEPTH` `task.fail`). The chain therefore nests workflows down to
197
+ * this depth, and the `agent` task in the depth-`MAX_WORKFLOW_DEPTH` workflow fails.
198
+ */
199
+ var MAX_WORKFLOW_DEPTH = 8;
200
+ /**
201
+ * The name under which the {@link import('./WorkflowRunner.js').WorkflowRunner} BINDS the
202
+ * depth/cycle-aware workflow tool onto a dispatched `agent` task's
203
+ * `AgentContextInterface` (the future `@orkestrel/agent` package, W-c2).
204
+ *
205
+ * @remarks
206
+ * The propagation seam's well-known key: before running an `agent` task, the runner adds a
207
+ * {@link import('./factories.js').createWorkflowTool}-built tool under this name to the
208
+ * resolved agent's `context.tools`, so the subagent can author + run a NESTED workflow
209
+ * (bounded by {@link MAX_WORKFLOW_DEPTH}). A subagent that wants to fan out into a workflow
210
+ * calls this tool by this name; the bound handler runs the nested workflow at depth + 1.
211
+ */
212
+ var WORKFLOW_TOOL_NAME = "workflow";
213
+ /**
214
+ * A complete FLAT authoring example — the PRIMARY way a small model authors a workflow
215
+ * through {@link import('./factories.js').createWorkflowTool}: `{ name, steps: [{ name, via }] }`.
216
+ *
217
+ * @remarks
218
+ * Each step becomes a one-task phase, in order; a step's `name` is a REGISTERED behavior name
219
+ * (not a label) and `via` is the execution mechanism. The tool expands this
220
+ * ({@link import('./helpers.js').expandSteps}) into a valid {@link WorkflowDefinition}. It
221
+ * is embedded VERBATIM in {@link WORKFLOW_TOOL_DESCRIPTION} and guarded by a parity test
222
+ * (it must expand to a tree the STRICT contract accepts), so the doc example can never drift.
223
+ */
224
+ var WORKFLOW_TOOL_FLAT_EXAMPLE = Object.freeze({
225
+ name: "release",
226
+ steps: Object.freeze([Object.freeze({
227
+ name: "compile",
228
+ via: "function"
229
+ }), Object.freeze({
230
+ name: "publish",
231
+ via: "tool"
232
+ })])
233
+ });
234
+ /**
235
+ * A minimal NESTED authoring example — the ADVANCED escape-hatch form a model may use
236
+ * instead of the flat shape: a full {@link WorkflowDefinition}.
237
+ *
238
+ * @remarks
239
+ * The full four-level form, documented in {@link WORKFLOW_TOOL_DESCRIPTION} as the advanced
240
+ * alternative. It is embedded VERBATIM and guarded by a parity test (`createWorkflowContract().is`
241
+ * must accept it), so the doc example can never drift from a valid definition.
242
+ */
243
+ var WORKFLOW_TOOL_NESTED_EXAMPLE = Object.freeze({
244
+ id: "release",
245
+ name: "Release",
246
+ phases: Object.freeze([Object.freeze({
247
+ id: "build",
248
+ name: "Build",
249
+ tasks: Object.freeze([Object.freeze({
250
+ id: "compile",
251
+ name: "Compile",
252
+ run: Object.freeze({
253
+ via: "function",
254
+ name: "compile"
255
+ })
256
+ })])
257
+ })])
258
+ });
259
+ /**
260
+ * The DESCRIPTION {@link import('./factories.js').createWorkflowTool} advertises — a
261
+ * multi-line guide that teaches a small model how to author a complete workflow tree.
262
+ *
263
+ * @remarks
264
+ * Presents the SIMPLE flat shape (`{ name, steps: [{ name, via }] }`) as the PRIMARY way with
265
+ * one complete worked example ({@link WORKFLOW_TOOL_FLAT_EXAMPLE}), names the three `via`
266
+ * values + that a step's `name` is a REGISTERED name (not a human label), and documents the full nested
267
+ * {@link WorkflowDefinition} as the ADVANCED form with a minimal example
268
+ * ({@link WORKFLOW_TOOL_NESTED_EXAMPLE}). Both examples are interpolated VERBATIM from the
269
+ * validated constants, so a parity test pins them — the description can never drift from a
270
+ * real, contract-valid example. The `parameters` the tool advertises are the FLAT shape's
271
+ * schema; the nested form is the documented escape-hatch (the tool accepts both).
272
+ */
273
+ var WORKFLOW_TOOL_DESCRIPTION = [
274
+ "Author and run a workflow (phases run sequentially, the tasks within a phase run concurrently) in one call.",
275
+ "",
276
+ "SIMPLEST way — a flat list of steps. Each step runs one registered behavior; steps run one after another:",
277
+ " { \"name\": \"<workflow name>\", \"steps\": [ { \"name\": \"<registered name>\", \"via\": \"function|tool|agent\" }, ... ] }",
278
+ "- a step's \"name\" is a REGISTERED behavior name (a registry key), NOT a human label.",
279
+ "- \"via\" is how to run it: \"function\" (the default if omitted), \"tool\", or \"agent\".",
280
+ "- the top-level \"name\" (the workflow name) is optional. Ids are filled in for you.",
281
+ "Example:",
282
+ JSON.stringify(WORKFLOW_TOOL_FLAT_EXAMPLE),
283
+ "",
284
+ "ADVANCED — the full nested form, for multi-task phases or explicit ids. A workflow has phases; a phase has tasks; a task has a \"run\" of { \"via\", \"name\" }:",
285
+ JSON.stringify(WORKFLOW_TOOL_NESTED_EXAMPLE),
286
+ "In the nested form you may omit any \"id\"/\"name\" and they are filled in positionally; a provided one is kept."
287
+ ].join("\n");
288
+ //#endregion
289
+ //#region src/core/errors.ts
290
+ /**
291
+ * An error thrown by the workflow entity + W-c2 recursion layer.
292
+ *
293
+ * @remarks
294
+ * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
295
+ * offending node id / status. Thrown for an illegal lifecycle transition
296
+ * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
297
+ * passed to {@link import('./factories.js').restoreWorkflow} (`RESTORE`), an over-deep /
298
+ * cyclic nested-workflow dispatch (`DEPTH`), and a malformed
299
+ * {@link import('./factories.js').createWorkflowTool} args blob (`TOOL`). On the
300
+ * workflow-tool seam the `DEPTH` / `TOOL` throw is ISOLATED by the
301
+ * `@orkestrel/agent` package's `ToolManager` into the tool result's
302
+ * top-level `error` (AGENTS §14 — the universal tool-handler contract).
303
+ */
304
+ var WorkflowError = class extends Error {
305
+ code;
306
+ context;
307
+ constructor(code, message, context) {
308
+ super(message);
309
+ this.name = "WorkflowError";
310
+ this.code = code;
311
+ this.context = context;
312
+ }
313
+ };
314
+ /**
315
+ * Narrow an unknown caught value to a {@link WorkflowError}.
316
+ *
317
+ * @param value - The value to test (typically a `catch` binding)
318
+ * @returns `true` when `value` is a {@link WorkflowError}
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * try {
323
+ * task.complete('done')
324
+ * } catch (error) {
325
+ * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
326
+ * }
327
+ * ```
328
+ */
329
+ function isWorkflowError(value) {
330
+ return value instanceof WorkflowError;
331
+ }
332
+ //#endregion
333
+ //#region src/core/helpers.ts
334
+ /**
335
+ * Narrow a {@link TaskForm} to the `function` form — a task that runs a registered
336
+ * function.
337
+ *
338
+ * @param form - The task form to test
339
+ * @returns `true` when `form.via` is `'function'`
340
+ */
341
+ function isFunctionTask(form) {
342
+ return form.via === "function";
343
+ }
344
+ /**
345
+ * Narrow a {@link TaskForm} to the `tool` form — a task that runs a registered tool.
346
+ *
347
+ * @param form - The task form to test
348
+ * @returns `true` when `form.via` is `'tool'`
349
+ */
350
+ function isToolTask(form) {
351
+ return form.via === "tool";
352
+ }
353
+ /**
354
+ * Narrow a {@link TaskForm} to the `agent` form — a task that runs a registered
355
+ * agent (a subagent).
356
+ *
357
+ * @param form - The task form to test
358
+ * @returns `true` when `form.via` is `'agent'`
359
+ */
360
+ function isAgentTask(form) {
361
+ return form.via === "agent";
362
+ }
363
+ /**
364
+ * The ancestry identifier of a workflow run — `workflow:<id>`.
365
+ *
366
+ * @remarks
367
+ * The {@link import('./WorkflowRunner.js').WorkflowRunner}'s cycle guard records one of
368
+ * these per workflow in the current nested run chain (carried on
369
+ * {@link import('./types.js').WorkflowRunOptions.ancestry}). Tagging the bare id keeps a
370
+ * workflow id and an {@link agentTag} agent name in ONE namespaced set without collision,
371
+ * so re-entering a workflow OR an agent already in the chain is a single `includes` check.
372
+ *
373
+ * @param id - The workflow definition's `id`
374
+ * @returns The namespaced ancestry tag (`workflow:<id>`)
375
+ */
376
+ function workflowTag(id) {
377
+ return `workflow:${id}`;
378
+ }
379
+ /**
380
+ * The ancestry identifier of an agent in a run chain — `agent:<name>`.
381
+ *
382
+ * @remarks
383
+ * The agent counterpart of {@link workflowTag}: the runner adds one when it dispatches an
384
+ * `agent` task, and rejects the task (a typed `DEPTH` `task.fail`) when the same tag is
385
+ * already in the ancestry (a re-entry cycle). The `agent:` namespace keeps it distinct
386
+ * from a same-string workflow id.
387
+ *
388
+ * @param name - The agent's registry name (the `agent`-form's `name`)
389
+ * @returns The namespaced ancestry tag (`agent:<name>`)
390
+ */
391
+ function agentTag(name) {
392
+ return `agent:${name}`;
393
+ }
394
+ /**
395
+ * Test whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
396
+ * transition further.
397
+ *
398
+ * @remarks
399
+ * The ONE terminal check across all three tiers (AGENTS §4.4 "one concept = one word"):
400
+ * a task, a phase, and a workflow share the same {@link LifecycleStatus} vocabulary, so a
401
+ * single predicate covers them — {@link derivePhaseStatus} and {@link deriveWorkflowStatus}
402
+ * both consult it to tell a settled node from an in-flight one. Terminal: `completed` /
403
+ * `failed` / `skipped` / `stopped`; the only non-terminal states are `pending` and
404
+ * `running`.
405
+ *
406
+ * @param status - The lifecycle status to test (a task / phase / workflow status)
407
+ * @returns `true` when the status is terminal
408
+ */
409
+ function isTerminalStatus(status) {
410
+ return status === "completed" || status === "failed" || status === "skipped" || status === "stopped";
411
+ }
412
+ /**
413
+ * Derive a phase's status from its tasks' statuses (tasks are concurrent, so this
414
+ * is an order-insensitive reduction).
415
+ *
416
+ * @remarks
417
+ * The truth table (most-severe terminal wins; `bail`-agnostic — a phase surfaces a
418
+ * task failure as `failed` so the workflow's `bail` policy can decide):
419
+ * - no tasks ⇒ `pending`.
420
+ * - any task `running`, OR a mix of started-and-unsettled tasks (some non-`pending`
421
+ * but not all terminal) ⇒ `running`.
422
+ * - every task `pending` ⇒ `pending`.
423
+ * - all terminal: any `failed` ⇒ `failed`; else any `stopped` ⇒ `stopped`; else any
424
+ * `completed` ⇒ `completed`; else (all `skipped`) ⇒ `skipped`.
425
+ *
426
+ * So an all-`skipped` phase is `skipped`, an all-`stopped` phase is `stopped`, a
427
+ * phase with completed tasks and some skips is `completed`, and a single failed
428
+ * task makes the phase `failed`.
429
+ *
430
+ * @param tasks - The phase's task statuses, in any order
431
+ * @returns The derived {@link PhaseStatus}
432
+ */
433
+ function derivePhaseStatus(tasks) {
434
+ if (tasks.length === 0) return "pending";
435
+ if (tasks.every((status) => status === "pending")) return "pending";
436
+ if (!tasks.every((status) => isTerminalStatus(status))) return "running";
437
+ if (tasks.some((status) => status === "failed")) return "failed";
438
+ if (tasks.some((status) => status === "stopped")) return "stopped";
439
+ if (tasks.some((status) => status === "completed")) return "completed";
440
+ return "skipped";
441
+ }
442
+ /**
443
+ * Derive a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
444
+ * paired with the EFFECTIVE `bail` it ran under (`phase.bail ?? workflow.bail`) — so the
445
+ * failure outcome is PER-PHASE-bail-aware (phases are sequential, but the derivation is an
446
+ * order-insensitive reduction over the settled set).
447
+ *
448
+ * @remarks
449
+ * `bail` is now a per-phase override (AGENTS §4.4), so it is carried on each
450
+ * {@link PhaseDerivation} rather than passed as one scalar. It is the ONLY axis that changes
451
+ * the failure outcome, decided per phase:
452
+ * - **A `failed` phase whose effective `bail` is `true` (halt)** propagates ⇒ the workflow is
453
+ * `failed` (the database-transaction halt) — even when the workflow default is graceful.
454
+ * - **A `failed` phase whose effective `bail` is `false` (graceful)** is DATA, not a workflow
455
+ * failure — it folds into completion like a settled phase. A graceful failed phase NEVER
456
+ * makes the workflow `failed` — even when the workflow default is strict.
457
+ *
458
+ * The rest of the table is shared:
459
+ * - no phases ⇒ `pending`.
460
+ * - any phase `running`, OR a mix of started-and-unsettled phases (some non-`pending`
461
+ * but not all terminal) ⇒ `running`.
462
+ * - every phase `pending` ⇒ `pending`.
463
+ * - all terminal (a `failed` phase counts as terminal here): any `stopped` ⇒ `stopped`; else
464
+ * any `completed` (or any graceful-bail `failed`, folded into completion) ⇒ `completed`;
465
+ * else (all `skipped`) ⇒ `skipped`.
466
+ *
467
+ * @param phases - The workflow's per-phase {@link PhaseDerivation}s (status + effective bail), in any order
468
+ * @returns The derived {@link WorkflowStatus}
469
+ */
470
+ function deriveWorkflowStatus(phases) {
471
+ if (phases.length === 0) return "pending";
472
+ if (phases.some((phase) => phase.status === "failed" && phase.bail)) return "failed";
473
+ if (phases.every((phase) => phase.status === "pending")) return "pending";
474
+ if (!phases.every((phase) => isTerminalStatus(phase.status))) return "running";
475
+ if (phases.some((phase) => phase.status === "stopped")) return "stopped";
476
+ if (phases.some((phase) => phase.status === "completed" || phase.status === "failed" && !phase.bail)) return "completed";
477
+ return "skipped";
478
+ }
479
+ /**
480
+ * Test whether the live W-b task state machine may move directly from one
481
+ * {@link TaskStatus} to another — the legal-transition guard.
482
+ *
483
+ * @remarks
484
+ * Reads the {@link import('./constants.js').TASK_TRANSITIONS} graph: `true` only when
485
+ * `to` is listed under `from`. A settled (terminal) `from` has no legal targets, so any
486
+ * transition off it is `false`. The W-b `Task` consults this before every transition and
487
+ * throws a `TRANSITION` {@link import('./errors.js').WorkflowError} when it returns `false`.
488
+ *
489
+ * @param from - The task's current status
490
+ * @param to - The status the transition would move it to
491
+ * @returns `true` when the move is legal
492
+ */
493
+ function canTransitionTask(from, to) {
494
+ return TASK_TRANSITIONS[from].includes(to);
495
+ }
496
+ /**
497
+ * Build a {@link WorkflowContext} — the identity every level inherits — from a node's
498
+ * `id` / `name` / optional `description`.
499
+ *
500
+ * @remarks
501
+ * The root of the context chain a live {@link import('./Workflow.js').Workflow} exposes;
502
+ * {@link buildPhaseContext} / {@link buildTaskContext} extend it down the tree. Accepts a
503
+ * structural node (a definition or a snapshot node — both carry the three identity fields).
504
+ *
505
+ * @param node - The node's identity (`id` / `name` / optional `description`)
506
+ * @returns The {@link WorkflowContext}
507
+ */
508
+ function buildWorkflowContext(node) {
509
+ return {
510
+ id: node.id,
511
+ name: node.name,
512
+ ...node.description === void 0 ? {} : { description: node.description }
513
+ };
514
+ }
515
+ /**
516
+ * Build a {@link PhaseContext} — a phase's own identity plus a back-reference to its
517
+ * workflow — from the parent {@link WorkflowContext} and the phase node's identity.
518
+ *
519
+ * @param workflow - The parent workflow context (the lineage pointer UP the tree)
520
+ * @param node - The phase's identity (`id` / `name` / optional `description`)
521
+ * @returns The {@link PhaseContext}
522
+ */
523
+ function buildPhaseContext(workflow, node) {
524
+ return {
525
+ ...buildWorkflowContext(node),
526
+ workflow
527
+ };
528
+ }
529
+ /**
530
+ * Build a {@link TaskContext} — a task's own identity plus a back-reference to its phase
531
+ * (and, transitively, its workflow) — from the parent {@link PhaseContext} and the task
532
+ * node's identity.
533
+ *
534
+ * @param phase - The parent phase context (carrying the full lineage UP the tree)
535
+ * @param node - The task's identity (`id` / `name` / optional `description`)
536
+ * @returns The {@link TaskContext}
537
+ */
538
+ function buildTaskContext(phase, node) {
539
+ return {
540
+ ...buildWorkflowContext(node),
541
+ phase
542
+ };
543
+ }
544
+ /**
545
+ * Narrow an `unknown` to a {@link WorkflowSnapshot} — the AGENTS §14 boundary guard for an
546
+ * UNTRUSTED snapshot read (a storage row a {@link import('./stores/DatabaseWorkflowStore.js').DatabaseWorkflowStore}
547
+ * reads back from its opaque JSON column, a snapshot loaded from disk).
548
+ *
549
+ * @remarks
550
+ * A total guard (it NEVER throws — adversarial input returns `false`, AGENTS §14). It checks the
551
+ * snapshot's SHAPE — `id` / `name` / `status` strings, a `boolean` `bail`, an array of `phases`,
552
+ * `created` / `updated` numbers — enough to safely impose the {@link WorkflowSnapshot} type at a
553
+ * storage boundary WITHOUT a cast. It is complementary to
554
+ * {@link import('./factories.js').assertSnapshot}, which validates the DEEPER invariant (every
555
+ * node's status / override drawn from the lifecycle vocabulary) and THROWS a `RESTORE`
556
+ * {@link import('./errors.js').WorkflowError} — the deep gate a {@link import('./factories.js').restoreWorkflow}
557
+ * applies. A boundary read narrows shape with this guard; a restore validates vocabulary with `assertSnapshot`.
558
+ *
559
+ * @param value - The value to test (an opaque storage read)
560
+ * @returns `true` when `value` has the structural shape of a {@link WorkflowSnapshot}
561
+ */
562
+ function isWorkflowSnapshot(value) {
563
+ return isRecord(value) && isString(value.id) && isString(value.name) && isString(value.status) && isBoolean(value.bail) && isArray(value.phases) && isNumber(value.created) && isNumber(value.updated);
564
+ }
565
+ /**
566
+ * Convert a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
567
+ * node `pending`, no results, empty metadata — so the live W-b tree has ONE construction
568
+ * path (snapshot-driven) for both a fresh build and a restore.
569
+ *
570
+ * @remarks
571
+ * The structural fields (`id` / `name` / `description` + the ordered phases / tasks)
572
+ * carry over verbatim; the W-b live tree is the DECLARATIVE state machine, so the
573
+ * execution-only definition fields (per-phase `run` / `concurrency`, per-task `retries` /
574
+ * `timeout`) are intentionally dropped (W-c reads them from the definition when it drives
575
+ * transitions). The `bail` policy carries over — at the workflow tier AND, per phase, the
576
+ * EFFECTIVE policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
577
+ * snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped now.
578
+ * {@link import('./factories.js').createWorkflow} builds from this.
579
+ *
580
+ * The optional `bail` override is the EFFECTIVE workflow policy the tree will run under
581
+ * (`createWorkflow` / the runner resolve `options.bail ?? definition.bail ?? DEFAULT_BAIL` and
582
+ * pass it here), so an `options.bail` override reaches BOTH the workflow tier AND the
583
+ * inheritance default of every phase that declares no `bail` of its own — otherwise the
584
+ * per-phase seeds would silently ignore the override. Omitted ⇒ the definition's own `bail`
585
+ * (defaulting to the graceful {@link import('./constants.js').DEFAULT_BAIL}).
586
+ *
587
+ * @param definition - The workflow definition to seed from
588
+ * @param bail - The EFFECTIVE workflow bail to seed both tiers with (defaults to the definition's)
589
+ * @returns An initial, all-`pending` {@link WorkflowSnapshot}
590
+ */
591
+ function definitionToSnapshot(definition, bail) {
592
+ const now = Date.now();
593
+ const workflowBail = bail ?? definition.bail ?? false;
594
+ return {
595
+ id: definition.id,
596
+ name: definition.name,
597
+ ...definition.description === void 0 ? {} : { description: definition.description },
598
+ status: "pending",
599
+ bail: workflowBail,
600
+ phases: definition.phases.map((phase) => phaseDefinitionToSnapshot(phase, workflowBail)),
601
+ created: now,
602
+ updated: now
603
+ };
604
+ }
605
+ /**
606
+ * Convert one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
607
+ * {@link PhaseSnapshot} — the per-phase step of {@link definitionToSnapshot}.
608
+ *
609
+ * @remarks
610
+ * The snapshot persists the EFFECTIVE failure policy this phase runs under: the phase's own
611
+ * `bail` when it declares one, else the `workflowBail` it inherits — so a restore reinstates
612
+ * the same per-phase policy without a silent default (`effectiveBail = phase.bail ?? workflowBail`).
613
+ *
614
+ * @param phase - The phase definition to seed from
615
+ * @param workflowBail - The workflow-level `bail` default the phase inherits when it declares none
616
+ * @returns An initial {@link PhaseSnapshot}
617
+ */
618
+ function phaseDefinitionToSnapshot(phase, workflowBail) {
619
+ return {
620
+ id: phase.id,
621
+ name: phase.name,
622
+ ...phase.description === void 0 ? {} : { description: phase.description },
623
+ status: "pending",
624
+ bail: phase.bail ?? workflowBail,
625
+ tasks: phase.tasks.map((task) => taskDefinitionToSnapshot(task))
626
+ };
627
+ }
628
+ /**
629
+ * Convert one {@link import('./types.js').TaskDefinition} into an initial, `pending`
630
+ * {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
631
+ * result yet, empty metadata).
632
+ *
633
+ * @param task - The task definition to seed from
634
+ * @returns An initial {@link TaskSnapshot}
635
+ */
636
+ function taskDefinitionToSnapshot(task) {
637
+ return {
638
+ id: task.id,
639
+ name: task.name,
640
+ ...task.description === void 0 ? {} : { description: task.description },
641
+ status: "pending",
642
+ metadata: {}
643
+ };
644
+ }
645
+ /**
646
+ * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
647
+ * — the workflow tier of the result tree, built from each phase's `results()`.
648
+ *
649
+ * @remarks
650
+ * Pure and order-preserving: phases in order, each phase's task results in order. The
651
+ * W-b `Workflow.results()` calls this over its phases' `results()`; a phase's own
652
+ * `results()` is the per-phase list this consumes.
653
+ *
654
+ * @param phases - The per-phase result lists, in phase order
655
+ * @returns One flattened {@link TaskResult} list, in positional order
656
+ */
657
+ function collectResults(phases) {
658
+ return phases.flat();
659
+ }
660
+ /**
661
+ * Summarize a terminal {@link WorkflowResult} into the PLAIN value a
662
+ * {@link import('./factories.js').createWorkflowTool} handler returns on success.
663
+ *
664
+ * @remarks
665
+ * This is the run summary the handler returns DIRECTLY — NOT a `ToolResult` (the future `@orkestrel/agent` package).
666
+ * The handler conforms to the universal tool-handler contract (AGENTS §14): it returns the plain value
667
+ * (and throws on failure), so the `@orkestrel/agent` package's `ToolManager` performs
668
+ * the ONE canonical wrap (`{ id, name, value }`) and the model reads exactly this summary — once,
669
+ * identically — over BOTH the agent loop and MCP. The summary is LEAN: the workflow's terminal `status`
670
+ * and the COUNT of settled task results — enough for a caller / model to react without serializing the
671
+ * whole live tree. (It carries no synthetic `id` / `name`: a tool handler has no call id; the manager
672
+ * supplies the canonical envelope's identity.)
673
+ *
674
+ * @param result - The terminal {@link WorkflowResult} the run produced
675
+ * @returns The plain success summary — `{ status, count }`
676
+ */
677
+ function workflowToolSummary(result) {
678
+ return {
679
+ status: result.status,
680
+ count: result.results.length
681
+ };
682
+ }
683
+ /**
684
+ * Complete a {@link WorkflowDraft} into a strict {@link WorkflowDefinition} — synthesize
685
+ * any MISSING `id` deterministically + positionally, and default any MISSING `name` to
686
+ * its (now-resolved) `id`.
687
+ *
688
+ * @remarks
689
+ * The positional id scheme is stable and human-legible: the workflow is `wf`, phase `i`
690
+ * is `phase-<i>`, and task `j` of that phase is `<phaseId>-task-<j>` (so a provided phase
691
+ * id flows into its tasks' synthesized ids). A PROVIDED `id` / `name` at any level is kept
692
+ * VERBATIM — synthesis touches only the omitted ones. A missing `name` defaults to the
693
+ * resolved `id` (never the other way round), so the result always has both. `run`,
694
+ * `description`, the per-phase `concurrency` / `bail`, the per-task `retries` / `timeout`, and
695
+ * the workflow `bail` carry over unchanged. The result is a complete
696
+ * {@link WorkflowDefinition}; the caller still validates it against the STRICT contract.
697
+ *
698
+ * @param draft - The draft workflow (id/name optional at all three levels)
699
+ * @returns A complete {@link WorkflowDefinition} with every id/name filled
700
+ */
701
+ function completeDraft(draft) {
702
+ const id = draft.id ?? "wf";
703
+ return {
704
+ id,
705
+ name: draft.name ?? id,
706
+ ...draft.description === void 0 ? {} : { description: draft.description },
707
+ phases: draft.phases.map((phase, index) => completePhaseDraft(phase, index)),
708
+ ...draft.bail === void 0 ? {} : { bail: draft.bail }
709
+ };
710
+ }
711
+ /**
712
+ * Complete one {@link PhaseDraft} into a strict {@link PhaseDefinition} — the per-phase
713
+ * step of {@link completeDraft} (phase `index` → `phase-<index>` when its id is omitted).
714
+ *
715
+ * @param phase - The draft phase
716
+ * @param index - The phase's positional index in the workflow
717
+ * @returns A complete {@link PhaseDefinition}
718
+ */
719
+ function completePhaseDraft(phase, index) {
720
+ const id = phase.id ?? `phase-${index}`;
721
+ return {
722
+ id,
723
+ name: phase.name ?? id,
724
+ ...phase.description === void 0 ? {} : { description: phase.description },
725
+ tasks: phase.tasks.map((task, taskIndex) => completeTaskDraft(task, id, taskIndex)),
726
+ ...phase.concurrency === void 0 ? {} : { concurrency: phase.concurrency },
727
+ ...phase.bail === void 0 ? {} : { bail: phase.bail }
728
+ };
729
+ }
730
+ /**
731
+ * Complete one {@link TaskDraft} into a strict {@link TaskDefinition} — the per-task leaf
732
+ * step of {@link completeDraft} (task `index` of phase `<phaseId>` → `<phaseId>-task-<index>`
733
+ * when its id is omitted).
734
+ *
735
+ * @param task - The draft task
736
+ * @param phaseId - The (resolved) parent phase id, so the synthesized task id nests under it
737
+ * @param index - The task's positional index within its phase
738
+ * @returns A complete {@link TaskDefinition}
739
+ */
740
+ function completeTaskDraft(task, phaseId, index) {
741
+ const id = task.id ?? `${phaseId}-task-${index}`;
742
+ return {
743
+ id,
744
+ name: task.name ?? id,
745
+ ...task.description === void 0 ? {} : { description: task.description },
746
+ run: task.run,
747
+ ...task.retries === void 0 ? {} : { retries: task.retries },
748
+ ...task.timeout === void 0 ? {} : { timeout: task.timeout }
749
+ };
750
+ }
751
+ /**
752
+ * Expand a flat {@link WorkflowSteps} blob into a strict {@link WorkflowDefinition} — each
753
+ * step becomes a one-task phase, IN ORDER.
754
+ *
755
+ * @remarks
756
+ * The expansion of the tool's ADVERTISED surface (AGENTS §21 — the simplest form a small
757
+ * model can author). Each {@link WorkflowStep} maps to a phase holding exactly one task:
758
+ * the step's `name` becomes the task's `run.name`, and its `via` becomes the task's `run.via`
759
+ * (defaulting to `'function'` when omitted). Ids/names are auto-filled positionally — it
760
+ * builds an ids-omitted {@link WorkflowDraft} and delegates to {@link completeDraft}, so the
761
+ * two lenient surfaces share ONE synthesis path (step `i` → phase `phase-<i>`, its task
762
+ * `phase-<i>-task-0`). The optional `name` becomes the workflow's `name`. The result is a
763
+ * complete definition the caller validates against the STRICT contract before running.
764
+ *
765
+ * @param flat - The flat steps blob (`{ name?, steps: [{ name, via? }] }`)
766
+ * @returns A complete {@link WorkflowDefinition} (one one-task phase per step)
767
+ */
768
+ function expandSteps(flat) {
769
+ return completeDraft({
770
+ ...flat.name === void 0 ? {} : { name: flat.name },
771
+ phases: flat.steps.map((step) => ({ tasks: [{ run: stepToForm(step) }] }))
772
+ });
773
+ }
774
+ /**
775
+ * Convert one flat {@link WorkflowStep} into a {@link TaskForm} — `name` → the form's `name`,
776
+ * `via` → the form's discriminant (defaulting to `'function'`).
777
+ *
778
+ * @param step - The flat step
779
+ * @returns The {@link TaskForm} the step's task runs
780
+ */
781
+ function stepToForm(step) {
782
+ return {
783
+ via: step.via ?? "function",
784
+ name: step.name
785
+ };
786
+ }
787
+ /**
788
+ * Create a {@link DeferredInterface} — a promise whose settlement is driven
789
+ * externally, so a caller can resolve/reject it from outside the executor.
790
+ *
791
+ * @typeParam T - The value the deferred promise resolves
792
+ * @returns A deferred `promise` plus its `resolve` / `reject`
793
+ */
794
+ function createDeferred() {
795
+ let resolve = () => {};
796
+ let reject = () => {};
797
+ return {
798
+ promise: new Promise((res, rej) => {
799
+ resolve = res;
800
+ reject = rej;
801
+ }),
802
+ resolve,
803
+ reject
804
+ };
805
+ }
806
+ //#endregion
807
+ //#region src/core/shapers.ts
808
+ /**
809
+ * The shape of a {@link import('./types.js').TaskForm} — a descriptive tagged union
810
+ * over the three execution mechanisms, discriminated by the `via` literal (never a
811
+ * bare `kind`; AGENTS §4.4). Each variant pairs the `via` discriminant with a `name`
812
+ * (the registry key for the behavior).
813
+ *
814
+ * @remarks
815
+ * The union and each `via` literal + `name` carry a `description` so the emitted JSON
816
+ * Schema spells out what the discriminant means and that `name` is a REGISTERED key
817
+ * (not a human label) — the field-level guidance a small model needs to fill `run`.
818
+ */
819
+ var taskFormShape = unionShape(objectShape({
820
+ via: literalShape(["function"], { description: "Run a registered workflow FUNCTION by name." }),
821
+ name: stringShape({
822
+ min: 1,
823
+ description: "The registered function name to invoke (a registry key, not a label)."
824
+ })
825
+ }), objectShape({
826
+ via: literalShape(["tool"], { description: "Run a registered TOOL by name." }),
827
+ name: stringShape({
828
+ min: 1,
829
+ description: "The registered tool name to invoke (a registry key, not a label)."
830
+ })
831
+ }), objectShape({
832
+ via: literalShape(["agent"], { description: "Run a registered AGENT (a subagent) by name." }),
833
+ name: stringShape({
834
+ min: 1,
835
+ description: "The registered agent name to invoke (a registry key, not a label)."
836
+ })
837
+ }));
838
+ /**
839
+ * The shape of a {@link import('./types.js').TaskDefinition} — identity plus the
840
+ * behavior reference ({@link taskFormShape}). `description` is optional prose.
841
+ */
842
+ var taskShape = objectShape({
843
+ id: stringShape({
844
+ min: 1,
845
+ description: "Unique task id within its phase."
846
+ }),
847
+ name: stringShape({
848
+ min: 1,
849
+ description: "Human-readable task name."
850
+ }),
851
+ description: optionalShape(stringShape({ description: "Optional task description." })),
852
+ run: taskFormShape,
853
+ retries: optionalShape(integerShape({
854
+ min: 0,
855
+ description: "Extra attempts after the first on failure; overrides the phase default. Omitted means none."
856
+ })),
857
+ timeout: optionalShape(integerShape({
858
+ min: 0,
859
+ description: "Per-attempt deadline in milliseconds; overrides the phase default. Omitted means no deadline."
860
+ }))
861
+ });
862
+ /**
863
+ * The shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
864
+ * {@link taskShape} tasks, and an optional positive-integer `concurrency` throttle
865
+ * (max tasks in flight; omitted ⇒ unbounded).
866
+ */
867
+ var phaseShape = objectShape({
868
+ id: stringShape({
869
+ min: 1,
870
+ description: "Unique phase id within the workflow."
871
+ }),
872
+ name: stringShape({
873
+ min: 1,
874
+ description: "Human-readable phase name."
875
+ }),
876
+ description: optionalShape(stringShape({ description: "Optional phase description." })),
877
+ tasks: arrayShape(taskShape, { description: "The phase tasks; they run CONCURRENTLY." }),
878
+ concurrency: optionalShape(integerShape({
879
+ min: 1,
880
+ description: "Max tasks in flight at once (a resource throttle); omitted means unbounded."
881
+ })),
882
+ bail: optionalShape(literalShape([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
883
+ });
884
+ /**
885
+ * The shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
886
+ * identity, its ordered {@link phaseShape} phases, and the optional `bail` boolean
887
+ * failure policy (the literal pair `true`/`false`, the runtime mirror of the boolean
888
+ * toggle; omitted ⇒ the graceful default).
889
+ */
890
+ var workflowShape = objectShape({
891
+ id: stringShape({
892
+ min: 1,
893
+ description: "Unique workflow id."
894
+ }),
895
+ name: stringShape({
896
+ min: 1,
897
+ description: "Human-readable workflow name."
898
+ }),
899
+ description: optionalShape(stringShape({ description: "Optional workflow description." })),
900
+ phases: arrayShape(phaseShape, { description: "The workflow phases; they run SEQUENTIALLY, in order." }),
901
+ bail: optionalShape(literalShape([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
902
+ });
903
+ /**
904
+ * The shape of a TASK in a draft workflow — identical to {@link taskShape} EXCEPT `id`
905
+ * and `name` are OPTIONAL (the tool synthesizes any missing one positionally).
906
+ *
907
+ * @remarks
908
+ * A PROVIDED `id` / `name` still carries `minLength: 1`, so an explicitly-empty `id: ''`
909
+ * is INVALID (rejected by the draft contract), never auto-filled — keeping "garbage"
910
+ * distinct from "omitted". `run` stays required.
911
+ */
912
+ var taskDraftShape = objectShape({
913
+ id: optionalShape(stringShape({
914
+ min: 1,
915
+ description: "Task id; auto-filled when omitted."
916
+ })),
917
+ name: optionalShape(stringShape({
918
+ min: 1,
919
+ description: "Task name; defaults to the id when omitted."
920
+ })),
921
+ description: optionalShape(stringShape({ description: "Optional task description." })),
922
+ run: taskFormShape,
923
+ retries: optionalShape(integerShape({
924
+ min: 0,
925
+ description: "Extra attempts after the first on failure; overrides the phase default. Omitted means none."
926
+ })),
927
+ timeout: optionalShape(integerShape({
928
+ min: 0,
929
+ description: "Per-attempt deadline in milliseconds; overrides the phase default. Omitted means no deadline."
930
+ }))
931
+ });
932
+ /**
933
+ * The shape of a PHASE in a draft workflow — identical to {@link phaseShape} EXCEPT
934
+ * `id` and `name` are OPTIONAL, and its tasks are {@link taskDraftShape}s.
935
+ */
936
+ var phaseDraftShape = objectShape({
937
+ id: optionalShape(stringShape({
938
+ min: 1,
939
+ description: "Phase id; auto-filled when omitted."
940
+ })),
941
+ name: optionalShape(stringShape({
942
+ min: 1,
943
+ description: "Phase name; defaults to the id when omitted."
944
+ })),
945
+ description: optionalShape(stringShape({ description: "Optional phase description." })),
946
+ tasks: arrayShape(taskDraftShape, { description: "The phase tasks; they run CONCURRENTLY." }),
947
+ concurrency: optionalShape(integerShape({
948
+ min: 1,
949
+ description: "Max tasks in flight at once (a resource throttle); omitted means unbounded."
950
+ })),
951
+ bail: optionalShape(literalShape([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
952
+ });
953
+ /**
954
+ * The shape of a DRAFT workflow — identical to {@link workflowShape} EXCEPT `id` and
955
+ * `name` are OPTIONAL at all three levels (workflow / phase / task), so a small model
956
+ * can omit the six identity strings and let the tool synthesize them positionally.
957
+ *
958
+ * @remarks
959
+ * The lenient counterpart {@link import('./factories.js').createWorkflowDraftContract}
960
+ * compiles. `run` stays required; a provided `id` / `name` still has `minLength: 1` (so an
961
+ * explicitly-empty `id: ''` is REJECTED, not auto-filled). After
962
+ * {@link import('./helpers.js').completeDraft} fills the missing ids/names, the result is
963
+ * validated against the STRICT {@link import('./factories.js').createWorkflowContract} gate
964
+ * before running.
965
+ */
966
+ var workflowDraftShape = objectShape({
967
+ id: optionalShape(stringShape({
968
+ min: 1,
969
+ description: "Workflow id; auto-filled when omitted."
970
+ })),
971
+ name: optionalShape(stringShape({
972
+ min: 1,
973
+ description: "Workflow name; defaults to the id when omitted."
974
+ })),
975
+ description: optionalShape(stringShape({ description: "Optional workflow description." })),
976
+ phases: arrayShape(phaseDraftShape, { description: "The workflow phases; they run SEQUENTIALLY, in order." }),
977
+ bail: optionalShape(literalShape([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
978
+ });
979
+ /**
980
+ * The shape of ONE flat step — `{ name, via? }` — the building block of
981
+ * {@link workflowStepsShape}.
982
+ *
983
+ * @remarks
984
+ * `name` is the REGISTERED behavior name the step runs (it becomes the task's `run.name`);
985
+ * `via` is the optional execution mechanism (defaults to `'function'` when omitted). The
986
+ * tool expands each step into a one-task phase, in order
987
+ * ({@link import('./helpers.js').expandSteps}).
988
+ */
989
+ var stepShape = objectShape({
990
+ name: stringShape({
991
+ min: 1,
992
+ description: "The registered behavior name this step runs (becomes the task run.name)."
993
+ }),
994
+ via: optionalShape(literalShape([
995
+ "function",
996
+ "tool",
997
+ "agent"
998
+ ], { description: "How to run it: function (default), tool, or agent." }))
999
+ });
1000
+ /**
1001
+ * The FLAT authoring shape `createWorkflowTool` advertises as its `parameters` — the
1002
+ * simplest surface a small model can fill: `{ name?, steps: [{ name, via? }] }`.
1003
+ *
1004
+ * @remarks
1005
+ * The deliberately-reduced surface (AGENTS §21): a flat ordered list of steps, each a
1006
+ * `{ name, via? }`. The tool EXPANDS it ({@link import('./helpers.js').expandSteps}) into a
1007
+ * full {@link import('./types.js').WorkflowDefinition} — one one-task phase per step, in
1008
+ * order — then validates against the STRICT
1009
+ * {@link import('./factories.js').createWorkflowContract} gate. The full nested form is
1010
+ * STILL accepted by the tool (it branches on the args' shape) and is documented as the
1011
+ * advanced escape-hatch in the tool's description — but THIS is what `parameters` advertises.
1012
+ */
1013
+ var workflowStepsShape = objectShape({
1014
+ name: optionalShape(stringShape({
1015
+ min: 1,
1016
+ description: "Optional workflow name."
1017
+ })),
1018
+ steps: arrayShape(stepShape, { description: "The ordered steps to run, one after another (each becomes a one-task phase)." })
1019
+ });
1020
+ //#endregion
1021
+ //#region src/core/stores/DatabaseWorkflowStore.ts
1022
+ /**
1023
+ * A {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
1024
+ * workflow's durable run-state IS a row, so persistence reduces to keyed point-access
1025
+ * (`get` / `set` / `delete`) over a `TableInterface`, the driver-pluggable twin of the
1026
+ * plain-`Map` {@link import('./MemoryWorkflowStore.js').MemoryWorkflowStore}.
1027
+ *
1028
+ * @remarks
1029
+ * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend
1030
+ * (memory, JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a
1031
+ * JSON / SQLite / IndexedDB backend swaps in WITHOUT touching the runner or the entity tree
1032
+ * — the same seam as `@orkestrel/queue`'s `DatabaseQueueStore`.
1033
+ * The driver defaults to memory ({@link import('../factories.js').createDatabaseWorkflowStore}
1034
+ * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
1035
+ * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
1036
+ *
1037
+ * The {@link WorkflowSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
1038
+ * `{ id; snapshot }` ({@link WorkflowSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
1039
+ * column the factory builds) — exactly as `DatabaseQueueStore` stores its `input`. The snapshot is
1040
+ * already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless AND
1041
+ * sidesteps a TS2589 instantiation-depth blow-up: a structured multi-column table would force the
1042
+ * contract to `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results),
1043
+ * tripping the compiler — one JSON column keeps the row type flat (`snapshot` reads back as `unknown`).
1044
+ *
1045
+ * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
1046
+ * the row `{ id: snapshot.id, snapshot }`.
1047
+ * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
1048
+ * a {@link WorkflowSnapshot} ({@link import('../helpers.js').isWorkflowSnapshot} — the AGENTS §14
1049
+ * boundary narrow for an untrusted storage read), or `undefined` if none is stored.
1050
+ * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
1051
+ *
1052
+ * UNLIKE the server package's `SessionStoreInterface` there is NO
1053
+ * idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
1054
+ * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
1055
+ * §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
1056
+ * snapshot back and rebuild the live tree with {@link import('../factories.js').restoreWorkflow}.
1057
+ *
1058
+ * @example
1059
+ * ```ts
1060
+ * import { createDatabaseWorkflowStore, createMemoryDriver, createWorkflow, restoreWorkflow } from '@src/core'
1061
+ *
1062
+ * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
1063
+ * const workflow = createWorkflow(definition)
1064
+ * await store.set(workflow.snapshot()) // persist the run state (one JSON column)
1065
+ * const snapshot = await store.get(definition.id)
1066
+ * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
1067
+ * await store.delete(definition.id) // drop it
1068
+ * ```
1069
+ */
1070
+ var DatabaseWorkflowStore = class {
1071
+ #table;
1072
+ /**
1073
+ * Wrap a table as a workflow store.
1074
+ *
1075
+ * @param table - The {@link TableInterface} holding the snapshots — its row is the
1076
+ * {@link WorkflowSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
1077
+ */
1078
+ constructor(table) {
1079
+ this.#table = table;
1080
+ }
1081
+ /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `WorkflowSnapshot`. */
1082
+ async get(id) {
1083
+ const row = await this.#table.get(id);
1084
+ if (row === void 0) return void 0;
1085
+ return isWorkflowSnapshot(row.snapshot) ? row.snapshot : void 0;
1086
+ }
1087
+ /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
1088
+ async set(snapshot) {
1089
+ await this.#table.set({
1090
+ id: snapshot.id,
1091
+ snapshot
1092
+ });
1093
+ }
1094
+ /** Drop a snapshot by id; an absent id is a no-op (no throw). */
1095
+ async delete(id) {
1096
+ await this.#table.remove(id);
1097
+ }
1098
+ };
1099
+ //#endregion
1100
+ //#region src/core/stores/MemoryWorkflowStore.ts
1101
+ /**
1102
+ * The in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
1103
+ * {@link WorkflowSnapshot}s keyed by workflow id, the DEFAULT store
1104
+ * {@link import('../factories.js').createMemoryWorkflowStore} builds.
1105
+ *
1106
+ * @remarks
1107
+ * A plain `Map<string, WorkflowSnapshot>` (AGENTS §21 — the snapshot is already pure,
1108
+ * self-contained JSON, so no encoding is needed for the memory tier). UNLIKE the server
1109
+ * package's `SessionStoreInterface`'s memory store there is
1110
+ * NO idle-TTL and NO eviction: a persisted workflow run-state is durable orchestration state
1111
+ * that lives until an explicit `delete`, never silently aging out (a run that vanished
1112
+ * mid-flight would be a silent data loss, not a freed session). A durable backend (JSON /
1113
+ * SQLite / IndexedDB) swaps in through the SAME interface without touching the runner or the
1114
+ * entity tree — its driver-pluggable twin is
1115
+ * {@link import('./DatabaseWorkflowStore.js').DatabaseWorkflowStore} (the snapshot as one opaque
1116
+ * JSON column), exactly as `@orkestrel/queue`'s `MemoryQueueStore`
1117
+ * twins `DatabaseQueueStore`.
1118
+ *
1119
+ * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
1120
+ * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
1121
+ * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
1122
+ *
1123
+ * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
1124
+ * bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
1125
+ * back and rebuild the live tree with {@link import('../factories.js').restoreWorkflow}.
1126
+ *
1127
+ * @example
1128
+ * ```ts
1129
+ * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@src/core'
1130
+ *
1131
+ * const store = createMemoryWorkflowStore()
1132
+ * const workflow = createWorkflow(definition)
1133
+ * await store.set(workflow.snapshot()) // persist the run state
1134
+ * const snapshot = await store.get(definition.id)
1135
+ * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
1136
+ * await store.delete(definition.id) // drop it
1137
+ * ```
1138
+ */
1139
+ var MemoryWorkflowStore = class {
1140
+ #snapshots = /* @__PURE__ */ new Map();
1141
+ get(id) {
1142
+ return Promise.resolve(this.#snapshots.get(id));
1143
+ }
1144
+ set(snapshot) {
1145
+ this.#snapshots.set(snapshot.id, snapshot);
1146
+ return Promise.resolve();
1147
+ }
1148
+ delete(id) {
1149
+ this.#snapshots.delete(id);
1150
+ return Promise.resolve();
1151
+ }
1152
+ };
1153
+ //#endregion
1154
+ //#region src/core/tasks/Task.ts
1155
+ /**
1156
+ * The live leaf state machine (W-b) for one task — an observable (AGENTS §13), guarded
1157
+ * synchronous task whose explicit {@link TaskStatus} advances through the AGENTS §10
1158
+ * transitions, recording a {@link TaskResult} on a terminal outcome.
1159
+ *
1160
+ * @remarks
1161
+ * - **Guarded transitions (AGENTS §10).** `start` (→ `running`), then `complete(value)`
1162
+ * (→ `completed`, records a {@link import('@orkestrel/contract').Success}), `fail(error)`
1163
+ * (→ `failed`, records a {@link import('@orkestrel/contract').Failure}), `skip` (→ `skipped`),
1164
+ * `stop` (→ `stopped`). Each consults {@link canTransitionTask} FIRST and throws a
1165
+ * `TRANSITION` {@link WorkflowError} on an illegal move (e.g. completing a non-`running`
1166
+ * task) — the legal graph is the single source of truth, so the leaf can never reach an
1167
+ * impossible state.
1168
+ * - **Override (snapshot fidelity).** `skip` / `stop` set `#override` to the forced terminal
1169
+ * status, so a RESTORE can tell a forced leaf (`skipped` / `stopped`) from a run-produced
1170
+ * one and reinstate it AS an override — preserving the round-trip.
1171
+ * - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
1172
+ * OWN event, THEN calls the parent phase's `#recompute` (injected at construction) so the
1173
+ * transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
1174
+ * order means an observer sees the CAUSE (this leaf changed) before the EFFECT (the parents
1175
+ * re-derive) — the project precedent (`Runner.#settle` emits its own `fail` before propagating).
1176
+ * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link TaskEventMap}) fires the
1177
+ * matching event strictly AFTER the state change, BEFORE the cascade; the emitter isolates
1178
+ * a listener throw and routes it to its `error` handler (the `error` option), so a buggy
1179
+ * observer can never corrupt a transition.
1180
+ */
1181
+ var Task = class {
1182
+ #context;
1183
+ #phase;
1184
+ #workflow;
1185
+ #recompute;
1186
+ #metadata;
1187
+ #emitter;
1188
+ #status;
1189
+ #result;
1190
+ constructor(context, phase, workflow, recompute, options, status = "pending", result) {
1191
+ this.#context = context;
1192
+ this.#phase = phase;
1193
+ this.#workflow = workflow;
1194
+ this.#recompute = recompute;
1195
+ this.#metadata = options?.metadata ?? {};
1196
+ this.#emitter = new Emitter({
1197
+ on: options?.on,
1198
+ error: options?.error
1199
+ });
1200
+ this.#status = status;
1201
+ this.#result = result;
1202
+ }
1203
+ get emitter() {
1204
+ return this.#emitter;
1205
+ }
1206
+ get id() {
1207
+ return this.#context.id;
1208
+ }
1209
+ get name() {
1210
+ return this.#context.name;
1211
+ }
1212
+ get description() {
1213
+ return this.#context.description;
1214
+ }
1215
+ get context() {
1216
+ return this.#context;
1217
+ }
1218
+ get phase() {
1219
+ return this.#phase;
1220
+ }
1221
+ get workflow() {
1222
+ return this.#workflow;
1223
+ }
1224
+ get status() {
1225
+ return this.#status;
1226
+ }
1227
+ get result() {
1228
+ return this.#result;
1229
+ }
1230
+ start() {
1231
+ this.#transition("running");
1232
+ this.#emitter.emit("start", this.id);
1233
+ this.#escalate();
1234
+ }
1235
+ complete(value) {
1236
+ this.#transition("completed");
1237
+ const result = this.#record("completed", {
1238
+ success: true,
1239
+ value
1240
+ });
1241
+ this.#emitter.emit("complete", result);
1242
+ this.#escalate();
1243
+ }
1244
+ fail(error) {
1245
+ this.#transition("failed");
1246
+ const reason = error instanceof Error ? error : new Error(String(error), { cause: error });
1247
+ const result = this.#record("failed", {
1248
+ success: false,
1249
+ error: reason
1250
+ });
1251
+ this.#emitter.emit("fail", result);
1252
+ this.#escalate();
1253
+ }
1254
+ skip() {
1255
+ this.#transition("skipped");
1256
+ this.#emitter.emit("skip");
1257
+ this.#escalate();
1258
+ }
1259
+ stop() {
1260
+ this.#transition("stopped");
1261
+ this.#emitter.emit("stop");
1262
+ this.#escalate();
1263
+ }
1264
+ snapshot() {
1265
+ return {
1266
+ id: this.id,
1267
+ name: this.name,
1268
+ ...this.description === void 0 ? {} : { description: this.description },
1269
+ status: this.#status,
1270
+ ...this.#result === void 0 ? {} : { result: this.#result },
1271
+ metadata: this.#metadata
1272
+ };
1273
+ }
1274
+ #transition(to) {
1275
+ if (!canTransitionTask(this.#status, to)) throw new WorkflowError("TRANSITION", `task '${this.id}' cannot transition from '${this.#status}' to '${to}'`, {
1276
+ task: this.id,
1277
+ from: this.#status,
1278
+ to
1279
+ });
1280
+ this.#status = to;
1281
+ }
1282
+ #record(status, result) {
1283
+ const record = {
1284
+ task: this.#context,
1285
+ phase: this.#context.phase,
1286
+ workflow: this.#context.phase.workflow,
1287
+ status,
1288
+ ...result === void 0 ? {} : { result },
1289
+ timestamp: Date.now()
1290
+ };
1291
+ this.#result = record;
1292
+ return record;
1293
+ }
1294
+ #escalate() {
1295
+ this.#recompute();
1296
+ }
1297
+ };
1298
+ //#endregion
1299
+ //#region src/core/tasks/TaskManager.ts
1300
+ /**
1301
+ * The lean child manager (AGENTS §9) of a {@link import('../phases/Phase.js').Phase}'s live
1302
+ * tasks — an insertion-ordered registry keyed by task `id`, so positional order is
1303
+ * preserved across an interior `skip` / `remove`.
1304
+ *
1305
+ * @remarks
1306
+ * - **Positional store.** Tasks live in an insertion-ordered `Map` keyed by `id`;
1307
+ * `append` adds one at the end (the build-time wiring path), `task(id)` looks one up,
1308
+ * `tasks()` lists them in positional order, `count` is the size. A `skip` is a STATUS
1309
+ * change on a stored task (never a removal), so order survives it; a snapshot RESTORE
1310
+ * re-`append`s in the snapshot's order, reproducing it exactly.
1311
+ * - **No batch matrix.** A phase's tasks are a fixed positional set, so AGENTS §9.2 (the
1312
+ * bulk verb overloads) is deliberately omitted — there is no `remove` family here.
1313
+ * - **Event-free.** A purely structural container — the live {@link TaskInterface}s own
1314
+ * their own emitters; the manager observes nothing.
1315
+ *
1316
+ * @example
1317
+ * ```ts
1318
+ * const tasks = new TaskManager()
1319
+ * tasks.append(task) // a live Task
1320
+ * tasks.task(task.id) // the same task
1321
+ * tasks.count // 1
1322
+ * ```
1323
+ */
1324
+ var TaskManager = class {
1325
+ #tasks = /* @__PURE__ */ new Map();
1326
+ get count() {
1327
+ return this.#tasks.size;
1328
+ }
1329
+ append(task) {
1330
+ this.#tasks.set(task.id, task);
1331
+ }
1332
+ task(id) {
1333
+ return this.#tasks.get(id);
1334
+ }
1335
+ tasks() {
1336
+ return [...this.#tasks.values()];
1337
+ }
1338
+ };
1339
+ //#endregion
1340
+ //#region src/core/phases/Phase.ts
1341
+ /**
1342
+ * The live DERIVED state machine (W-b) for one phase — an observable (AGENTS §13) whose
1343
+ * {@link PhaseStatus} is computed from its tasks (never set directly) and recomputed
1344
+ * reactively as a task transitions (the middle tier of the cascade).
1345
+ *
1346
+ * @remarks
1347
+ * - **Derived status.** `status` is `#override` when one is in force, else
1348
+ * {@link derivePhaseStatus} over the live tasks' statuses. {@link #recompute} (passed to
1349
+ * each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
1350
+ * event AND escalates to the workflow ({@link #escalate}, the upward step of the cascade).
1351
+ * - **Override (AGENTS §10).** `skip` / `stop` FORCE the phase's status (e.g. skipping a whole
1352
+ * phase), overriding the derived value; the override is PERSISTED in the snapshot's own
1353
+ * `override` field and restored DIRECTLY (no divergence guess), so a forced phase round-trips.
1354
+ * - **Children (AGENTS §9).** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
1355
+ * no batch matrix); built positionally from the snapshot so order survives an interior `skip`.
1356
+ * `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
1357
+ * tree); `workflow` navigates UP to the live parent.
1358
+ * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link PhaseEventMap}) fires
1359
+ * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE, strictly AFTER the
1360
+ * recompute + escalate; the emitter isolates a listener throw and routes it to its `error`
1361
+ * handler (the `error` option); `fail` carries the failing task's {@link TaskResult}.
1362
+ */
1363
+ var Phase = class {
1364
+ #context;
1365
+ #workflow;
1366
+ #escalateUp;
1367
+ #tasks = new TaskManager();
1368
+ #bail;
1369
+ #emitter;
1370
+ #status;
1371
+ #override;
1372
+ constructor(snapshot, workflow, escalate, options, bail) {
1373
+ this.#context = {
1374
+ id: snapshot.id,
1375
+ name: snapshot.name,
1376
+ workflow: workflow.context
1377
+ };
1378
+ if (snapshot.description !== void 0) this.#context = {
1379
+ ...this.#context,
1380
+ description: snapshot.description
1381
+ };
1382
+ this.#workflow = workflow;
1383
+ this.#escalateUp = escalate;
1384
+ this.#bail = bail ?? snapshot.bail;
1385
+ this.#emitter = new Emitter({
1386
+ on: options?.on,
1387
+ error: options?.error
1388
+ });
1389
+ for (const task of snapshot.tasks) this.#append(task, options);
1390
+ this.#override = snapshot.override;
1391
+ this.#status = this.status;
1392
+ }
1393
+ get emitter() {
1394
+ return this.#emitter;
1395
+ }
1396
+ get id() {
1397
+ return this.#context.id;
1398
+ }
1399
+ get name() {
1400
+ return this.#context.name;
1401
+ }
1402
+ get description() {
1403
+ return this.#context.description;
1404
+ }
1405
+ get context() {
1406
+ return this.#context;
1407
+ }
1408
+ get workflow() {
1409
+ return this.#workflow;
1410
+ }
1411
+ get bail() {
1412
+ return this.#bail;
1413
+ }
1414
+ get status() {
1415
+ return this.#override ?? derivePhaseStatus(this.#statuses());
1416
+ }
1417
+ get tasks() {
1418
+ return this.#tasks;
1419
+ }
1420
+ task(id) {
1421
+ return this.#tasks.task(id);
1422
+ }
1423
+ results() {
1424
+ const results = [];
1425
+ for (const task of this.#tasks.tasks()) if (task.result !== void 0) results.push(task.result);
1426
+ return results;
1427
+ }
1428
+ skip() {
1429
+ this.#force("skipped");
1430
+ }
1431
+ stop() {
1432
+ this.#force("stopped");
1433
+ }
1434
+ snapshot() {
1435
+ return {
1436
+ id: this.id,
1437
+ name: this.name,
1438
+ ...this.description === void 0 ? {} : { description: this.description },
1439
+ status: this.status,
1440
+ ...this.#override === void 0 ? {} : { override: this.#override },
1441
+ bail: this.#bail,
1442
+ tasks: this.#tasks.tasks().map((task) => task.snapshot())
1443
+ };
1444
+ }
1445
+ #recompute() {
1446
+ const next = this.status;
1447
+ if (next === this.#status) {
1448
+ this.#escalateUp();
1449
+ return;
1450
+ }
1451
+ this.#status = next;
1452
+ this.#emitFor(next);
1453
+ this.#escalateUp();
1454
+ }
1455
+ #force(status) {
1456
+ this.#override = status;
1457
+ this.#recompute();
1458
+ }
1459
+ #emitFor(status) {
1460
+ if (status === "running") this.#emitter.emit("start", this.id);
1461
+ else if (status === "completed") this.#emitter.emit("complete");
1462
+ else if (status === "failed") this.#emitter.emit("fail", this.#failure());
1463
+ else if (status === "stopped") this.#emitter.emit("stop");
1464
+ }
1465
+ #failure() {
1466
+ for (const task of this.#tasks.tasks()) {
1467
+ const result = task.result;
1468
+ if (result?.result?.success === false) return result;
1469
+ }
1470
+ throw new Error(`phase '${this.id}' derived failed with no failing task result`);
1471
+ }
1472
+ #append(task, options) {
1473
+ const created = new Task(buildTaskContext(this.#context, task), this, this.#workflow, () => this.#recompute(), options?.tasks?.[task.id], task.status, task.result);
1474
+ this.#tasks.append(created);
1475
+ }
1476
+ #statuses() {
1477
+ return this.#tasks.tasks().map((task) => task.status);
1478
+ }
1479
+ };
1480
+ //#endregion
1481
+ //#region src/core/phases/PhaseManager.ts
1482
+ /**
1483
+ * The lean child manager (AGENTS §9) of a {@link import('../Workflow.js').Workflow}'s
1484
+ * live phases — an insertion-ordered registry keyed by phase `id`, the phase analogue
1485
+ * of {@link import('../tasks/TaskManager.js').TaskManager}.
1486
+ *
1487
+ * @remarks
1488
+ * - **Positional store.** Phases live in an insertion-ordered `Map` keyed by `id`;
1489
+ * `append` adds one at the end, `phase(id)` looks one up, `phases()` lists them in
1490
+ * positional order, `count` is the size. A snapshot RESTORE re-`append`s in the
1491
+ * snapshot's order, reproducing it exactly.
1492
+ * - **No batch matrix.** A workflow's phases are a fixed positional set, so AGENTS §9.2
1493
+ * is deliberately omitted.
1494
+ * - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own
1495
+ * their own emitters.
1496
+ *
1497
+ * @example
1498
+ * ```ts
1499
+ * const phases = new PhaseManager()
1500
+ * phases.append(phase) // a live Phase
1501
+ * phases.phase(phase.id) // the same phase
1502
+ * phases.count // 1
1503
+ * ```
1504
+ */
1505
+ var PhaseManager = class {
1506
+ #phases = /* @__PURE__ */ new Map();
1507
+ get count() {
1508
+ return this.#phases.size;
1509
+ }
1510
+ append(phase) {
1511
+ this.#phases.set(phase.id, phase);
1512
+ }
1513
+ phase(id) {
1514
+ return this.#phases.get(id);
1515
+ }
1516
+ phases() {
1517
+ return [...this.#phases.values()];
1518
+ }
1519
+ };
1520
+ //#endregion
1521
+ //#region src/core/Workflow.ts
1522
+ /**
1523
+ * The live DERIVED state machine (W-b) for a whole workflow — the observable (AGENTS §13)
1524
+ * ROOT whose {@link WorkflowStatus} is computed from its phases under the `bail` policy and
1525
+ * recomputed reactively as the cascade propagates up from a task transition.
1526
+ *
1527
+ * @remarks
1528
+ * - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
1529
+ * {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
1530
+ * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').restoreWorkflow}
1531
+ * passes a persisted one). Each child {@link Phase} is wired to escalate to {@link #recompute}.
1532
+ * - **Derived status.** `status` is `#override` when forced, else
1533
+ * {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
1534
+ * reachable ONLY under `bail: true` (a single failed task halts the workflow); under
1535
+ * `bail: false` a failed phase folds into `completed`. {@link #recompute} diffs on each phase
1536
+ * change; a CHANGE emits.
1537
+ * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; the override is PERSISTED in the
1538
+ * snapshot's own `override` field and restored DIRECTLY (no divergence guess). The snapshot also
1539
+ * persists `bail`, so a restore re-derives status identically without a silent policy default.
1540
+ * - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
1541
+ * workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
1542
+ * navigate UP.
1543
+ * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
1544
+ * JSON); {@link import('./factories.js').restoreWorkflow} rebuilds an equivalent live tree.
1545
+ * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
1546
+ * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE; the emitter isolates a
1547
+ * listener throw and routes it to its `error` handler (the `error` option); `fail` carries
1548
+ * the failing task's {@link TaskResult}.
1549
+ */
1550
+ var Workflow = class {
1551
+ #context;
1552
+ #bail;
1553
+ #bailOverride;
1554
+ #phases = new PhaseManager();
1555
+ #emitter;
1556
+ #created;
1557
+ #updated;
1558
+ #status;
1559
+ #override;
1560
+ constructor(snapshot, options) {
1561
+ this.#context = buildWorkflowContext(snapshot);
1562
+ this.#bail = options?.bail ?? snapshot.bail;
1563
+ this.#bailOverride = options?.bail;
1564
+ this.#emitter = new Emitter({
1565
+ on: options?.on,
1566
+ error: options?.error
1567
+ });
1568
+ this.#created = snapshot.created;
1569
+ this.#updated = snapshot.updated;
1570
+ for (const phase of snapshot.phases) this.#append(phase, options);
1571
+ this.#override = snapshot.override;
1572
+ this.#status = this.status;
1573
+ }
1574
+ get emitter() {
1575
+ return this.#emitter;
1576
+ }
1577
+ get id() {
1578
+ return this.#context.id;
1579
+ }
1580
+ get name() {
1581
+ return this.#context.name;
1582
+ }
1583
+ get description() {
1584
+ return this.#context.description;
1585
+ }
1586
+ get context() {
1587
+ return this.#context;
1588
+ }
1589
+ get bail() {
1590
+ return this.#bail;
1591
+ }
1592
+ get status() {
1593
+ return this.#override ?? deriveWorkflowStatus(this.#statuses());
1594
+ }
1595
+ get phases() {
1596
+ return this.#phases;
1597
+ }
1598
+ phase(id) {
1599
+ return this.#phases.phase(id);
1600
+ }
1601
+ results() {
1602
+ return collectResults(this.#phases.phases().map((phase) => phase.results()));
1603
+ }
1604
+ skip() {
1605
+ this.#force("skipped");
1606
+ }
1607
+ stop() {
1608
+ this.#force("stopped");
1609
+ }
1610
+ complete() {
1611
+ this.#force("completed");
1612
+ }
1613
+ snapshot() {
1614
+ return {
1615
+ id: this.id,
1616
+ name: this.name,
1617
+ ...this.description === void 0 ? {} : { description: this.description },
1618
+ status: this.status,
1619
+ ...this.#override === void 0 ? {} : { override: this.#override },
1620
+ bail: this.#bail,
1621
+ phases: this.#phases.phases().map((phase) => phase.snapshot()),
1622
+ created: this.#created,
1623
+ updated: this.#updated
1624
+ };
1625
+ }
1626
+ #recompute() {
1627
+ const next = this.status;
1628
+ if (next === this.#status) return;
1629
+ this.#status = next;
1630
+ this.#updated = Date.now();
1631
+ this.#emitFor(next);
1632
+ }
1633
+ #force(status) {
1634
+ this.#override = status;
1635
+ this.#recompute();
1636
+ }
1637
+ #emitFor(status) {
1638
+ if (status === "running") this.#emitter.emit("start", this.id);
1639
+ else if (status === "completed") this.#emitter.emit("complete");
1640
+ else if (status === "failed") this.#emitter.emit("fail", this.#failure());
1641
+ else if (status === "stopped") this.#emitter.emit("stop");
1642
+ }
1643
+ #failure() {
1644
+ for (const result of this.results()) if (result.result?.success === false) return result;
1645
+ throw new Error(`workflow '${this.id}' derived failed with no failing task result`);
1646
+ }
1647
+ #append(phase, options) {
1648
+ const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride);
1649
+ this.#phases.append(created);
1650
+ }
1651
+ #statuses() {
1652
+ return this.#phases.phases().map((phase) => ({
1653
+ status: phase.status,
1654
+ bail: phase.bail
1655
+ }));
1656
+ }
1657
+ };
1658
+ //#endregion
1659
+ //#region src/core/Controller.ts
1660
+ /**
1661
+ * The per-unit handle a runner handler receives — wraps the unit's identity,
1662
+ * input, cancellation, and the run controls (`wait` / `spawn` / `abort`).
1663
+ *
1664
+ * @remarks
1665
+ * - **Built by the Runner per unit.** The runner constructs one `Controller` per
1666
+ * unit it dispatches, handing it the unit's `id`, `input`, the unit's `Abort`
1667
+ * handle, the queue attempt's `signal`, and a `spawn` callback that launches a
1668
+ * sibling through the same queue.
1669
+ * - **Signal.** `signal` is the queue attempt's signal, which ANY-combines the
1670
+ * unit's own abort, the runner-level abort (the runner aborts every unit), and
1671
+ * the per-attempt timeout — so it fires on any of the three. `aborted` and
1672
+ * `abort(reason)` delegate to the unit's `Abort` (the cancellation source of
1673
+ * truth); since the attempt signal ANY-includes that abort, `abort()` fires
1674
+ * `signal` too.
1675
+ * - **`wait` promise-parks (never a timer).** It resolves the instant the unit's
1676
+ * `signal` fires (immediately if already aborted) via a one-shot listener — no
1677
+ * `setTimeout`, no polling, no busy-yield — so a parked unit costs no CPU.
1678
+ * - **`spawn` is fire-and-track.** It delegates to the runner's launch-a-sibling
1679
+ * callback, which routes the sibling through the queue; the runner's `execute`
1680
+ * awaits the spawn closure, so the sibling runs whether or not its promise is
1681
+ * awaited. (Inline-awaiting a spawn from a slot-holding handler on a bounded
1682
+ * runner can deadlock — fan out instead; see {@link ControllerInterface.spawn}.)
1683
+ * - **Event-free by design.** The per-unit handle carries no Emitter; observe the
1684
+ * {@link RunnerInterface.emitter} instead (`unit` / `spawn` / `settle` / `fail` carry the id).
1685
+ */
1686
+ var Controller = class {
1687
+ id;
1688
+ input;
1689
+ signal;
1690
+ #abort;
1691
+ #spawn;
1692
+ constructor(id, input, abort, signal, spawn) {
1693
+ this.id = id;
1694
+ this.input = input;
1695
+ this.#abort = abort;
1696
+ this.signal = signal;
1697
+ this.#spawn = spawn;
1698
+ }
1699
+ get aborted() {
1700
+ return this.#abort.aborted;
1701
+ }
1702
+ wait() {
1703
+ if (this.signal.aborted) return Promise.resolve();
1704
+ return new Promise((resolve) => {
1705
+ this.signal.addEventListener("abort", () => resolve(), { once: true });
1706
+ });
1707
+ }
1708
+ spawn(input) {
1709
+ return this.#spawn(input);
1710
+ }
1711
+ abort(reason) {
1712
+ this.#abort.abort(reason);
1713
+ }
1714
+ };
1715
+ //#endregion
1716
+ //#region src/core/Runner.ts
1717
+ /**
1718
+ * A thin generic orchestrator that drives declared units — and any they `spawn` —
1719
+ * through a bounded-concurrency {@link createQueue}, collecting ordered results.
1720
+ *
1721
+ * @remarks
1722
+ * - **Drives the Queue (no reimplemented concurrency).** Every unit (declared or
1723
+ * spawned) is `enqueue`d on one internal `Queue`, so backpressure, FIFO ordering,
1724
+ * bounded concurrency, retries, and the per-attempt timeout are all the Queue's —
1725
+ * the Runner adds only orchestration (launching, ordering, draining, fail-fast).
1726
+ * - **Spawns actually run, results stay ordered (the B2 fix).** Declared inputs and
1727
+ * `spawn`ed siblings flow through the SAME `#launch`, which appends the unit's `id`
1728
+ * to an ordered `#order` list and records its settled value into `#values` by `id`.
1729
+ * Results are read back as `#order.map(id => #values.get(id))` — declared first (in
1730
+ * input order), then spawns (in spawn order). There is no one-time task snapshot,
1731
+ * so a unit spawned mid-handler is run and ordered like any other.
1732
+ * - **`execute` awaits the full spawn closure via a count gate.** `#launch` increments
1733
+ * an outstanding-unit `#count` BEFORE enqueuing and every settle decrements it,
1734
+ * resolving the `#drained` deferred at zero. Because `spawn` calls `#launch` (so
1735
+ * `#count += 1`) before the parent handler returns, the count never reaches zero
1736
+ * mid-run — `execute` parks on `#drained` and so awaits the entire transitive
1737
+ * closure, not just the declared units.
1738
+ * - **`spawn` is fire-and-track.** A spawned unit runs through the queue regardless of
1739
+ * whether its promise is awaited; the Runner never awaits a spawned promise from
1740
+ * within a handler's slot (it awaits the count gate instead), so a slot-holding
1741
+ * handler can fan out without the Runner deadlocking it. (An inline `await` of a
1742
+ * spawn by a bounded handler can still deadlock — that caveat is the caller's.)
1743
+ * - **Per-unit Controller + signal.** Each unit gets a `Controller` carrying its `id`,
1744
+ * `input`, the unit's `Abort` (so `aborted` / `abort` delegate to it), and the queue
1745
+ * attempt's `signal` (which ANY-combines the unit abort + runner abort + timeout). A
1746
+ * `spawn` callback is injected so `controller.spawn(input)` delegates to `#launch`.
1747
+ * - **One-shot + fail-fast.** `execute` runs once (a second call throws). The first
1748
+ * unit failure (after its retries) records the error and `abort()`s the run, so every
1749
+ * sibling's signal fires; later failures are ignored and `execute` rejects with the
1750
+ * first error. A user `abort(reason)` likewise rejects a running `execute`.
1751
+ * - **Observable (§13).** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
1752
+ * lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
1753
+ * fire-and-forget observers. Every event is emitted directly, strictly AFTER the relevant
1754
+ * launch / settle / drain transition; the emitter isolates a listener throw and routes it
1755
+ * to its `error` handler (the `error` option), so a buggy observer can NEVER reorder, throw
1756
+ * into, or corrupt the one-shot / fail-fast / spawn-tracking engine: the outstanding-unit
1757
+ * count gate stays balanced and fail-fast still fires regardless of what a listener does.
1758
+ * Observation is purely a side-channel.
1759
+ */
1760
+ var Runner = class {
1761
+ #handler;
1762
+ #entries;
1763
+ #queue;
1764
+ #emitter;
1765
+ #aborts = /* @__PURE__ */ new Map();
1766
+ #order = [];
1767
+ #values = /* @__PURE__ */ new Map();
1768
+ #count = 0;
1769
+ #drained;
1770
+ #started = false;
1771
+ #running = false;
1772
+ #stopped = false;
1773
+ #failure;
1774
+ constructor(options) {
1775
+ this.#handler = options.handler;
1776
+ this.#entries = options.entries;
1777
+ this.#emitter = new Emitter({
1778
+ on: options?.on,
1779
+ error: options?.error
1780
+ });
1781
+ this.#queue = createQueue({
1782
+ handler: (unit, execution) => this.#dispatch(unit, execution),
1783
+ concurrency: options.concurrency,
1784
+ retries: options.retries,
1785
+ timeout: options.timeout
1786
+ });
1787
+ }
1788
+ get emitter() {
1789
+ return this.#emitter;
1790
+ }
1791
+ get active() {
1792
+ return this.#count;
1793
+ }
1794
+ get stopped() {
1795
+ return this.#stopped;
1796
+ }
1797
+ async execute(inputs) {
1798
+ if (this.#started) throw new Error("runner has already executed");
1799
+ if (this.#stopped) throw new Error("runner is stopped");
1800
+ this.#started = true;
1801
+ this.#running = true;
1802
+ this.#emitter.emit("start");
1803
+ if (inputs.length === 0) {
1804
+ this.#running = false;
1805
+ this.#emitter.emit("finish", []);
1806
+ return [];
1807
+ }
1808
+ const drained = createDeferred();
1809
+ this.#drained = drained;
1810
+ for (const input of inputs) this.#launch(input);
1811
+ await drained.promise;
1812
+ this.#running = false;
1813
+ if (this.#failure !== void 0) throw this.#failure.error;
1814
+ const results = this.#collect();
1815
+ this.#emitter.emit("finish", results);
1816
+ return results;
1817
+ }
1818
+ abort(reason) {
1819
+ if (this.#stopped) return;
1820
+ if (this.#running && this.#failure === void 0) this.#failure = { error: reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason };
1821
+ this.#cancel(reason);
1822
+ this.#queue.abort(reason);
1823
+ this.#stopped = true;
1824
+ this.#emitter.emit("abort", reason);
1825
+ }
1826
+ destroy() {
1827
+ if (this.#stopped) {
1828
+ this.#queue.destroy();
1829
+ return;
1830
+ }
1831
+ this.abort();
1832
+ this.#queue.destroy();
1833
+ }
1834
+ #launch(input, parent) {
1835
+ const id = crypto.randomUUID();
1836
+ const abort = createAbort();
1837
+ this.#aborts.set(id, abort);
1838
+ this.#order.push(id);
1839
+ this.#count += 1;
1840
+ if (parent !== void 0) this.#emitter.emit("spawn", id, parent);
1841
+ const promise = this.#queue.enqueue({
1842
+ id,
1843
+ input
1844
+ }, {
1845
+ id,
1846
+ signal: abort.signal,
1847
+ ...this.#entries?.(input)
1848
+ });
1849
+ promise.then((value) => this.#settle(id, {
1850
+ ok: true,
1851
+ value
1852
+ }), (error) => this.#settle(id, {
1853
+ ok: false,
1854
+ error
1855
+ }));
1856
+ return promise;
1857
+ }
1858
+ #dispatch(unit, execution) {
1859
+ const abort = this.#aborts.get(unit.id);
1860
+ if (abort === void 0) throw new Error("unit abort missing");
1861
+ const controller = new Controller(unit.id, unit.input, abort, execution.signal, (input) => this.#spawn(input, unit.id));
1862
+ this.#emitter.emit("unit", unit.id);
1863
+ return this.#handler(controller);
1864
+ }
1865
+ #spawn(input, parent) {
1866
+ if (!this.#running) throw new Error("spawn is unavailable outside an active run");
1867
+ return this.#launch(input, parent);
1868
+ }
1869
+ #settle(id, outcome) {
1870
+ if (outcome.ok) {
1871
+ this.#values.set(id, { value: outcome.value });
1872
+ this.#emitter.emit("settle", id);
1873
+ } else if (this.#failure === void 0) {
1874
+ this.#failure = { error: outcome.error };
1875
+ this.#emitter.emit("fail", id, outcome.error);
1876
+ this.abort(outcome.error);
1877
+ }
1878
+ this.#count -= 1;
1879
+ if (this.#count === 0) this.#drained?.resolve();
1880
+ }
1881
+ #collect() {
1882
+ const results = [];
1883
+ for (const id of this.#order) {
1884
+ const box = this.#values.get(id);
1885
+ if (box !== void 0) results.push(box.value);
1886
+ }
1887
+ return results;
1888
+ }
1889
+ #cancel(reason) {
1890
+ for (const abort of this.#aborts.values()) abort.abort(reason);
1891
+ }
1892
+ };
1893
+ //#endregion
1894
+ //#region src/core/tasks/TaskController.ts
1895
+ /**
1896
+ * The lean per-task handle a {@link import('./types.js').WorkflowFunction} receives — the
1897
+ * running task's folded cancellation, its input, its lineage, and read-UP access to the
1898
+ * result tree.
1899
+ *
1900
+ * @remarks
1901
+ * - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
1902
+ * declarative W-b tree, not a fan-out unit, so this carries none of the runner
1903
+ * `Controller`'s `spawn` / `wait` — only what a leaf needs.
1904
+ * - **Folded signal.** `signal` is the cancellation the runner folds for THIS run: it fires
1905
+ * on a workflow-level abort / timeout / budget ceiling, or — under `bail: true` — when a
1906
+ * sibling task fails (the runner aborts the in-flight siblings via the substrate's
1907
+ * fail-fast). A handler races its work against it; `aborted` reads it.
1908
+ * - **Input + lineage.** `input` is the task's open `metadata` bag (its
1909
+ * {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
1910
+ * {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate UP the lineage.
1911
+ * - **Read-up results.** `results()` returns every settled task's {@link TaskResult} across
1912
+ * the phases that have already finished (a closure over the live
1913
+ * {@link import('./types.js').WorkflowInterface}), so a `function` task can read an earlier
1914
+ * phase's output. Read-only — a task records its OWN outcome by returning / throwing, not
1915
+ * by mutating the tree.
1916
+ * - **Event-free.** Like the runner `Controller`, the per-task handle carries no Emitter;
1917
+ * observe the W-b entities' own emitters (`task.emitter` / `phase.emitter`) instead.
1918
+ */
1919
+ var TaskController = class {
1920
+ signal;
1921
+ input;
1922
+ task;
1923
+ #results;
1924
+ constructor(signal, input, task, results) {
1925
+ this.signal = signal;
1926
+ this.input = input;
1927
+ this.task = task;
1928
+ this.#results = results;
1929
+ }
1930
+ get aborted() {
1931
+ return this.signal.aborted;
1932
+ }
1933
+ results() {
1934
+ return this.#results();
1935
+ }
1936
+ };
1937
+ //#endregion
1938
+ //#region src/core/WorkflowRunner.ts
1939
+ /**
1940
+ * The thin orchestrator that EXECUTES a live W-b workflow tree by COMPOSING the shipped
1941
+ * substrate — phases sequential, tasks concurrent — dispatching each task BY NAME under the
1942
+ * `bail` policy, including the W-c2 `agent` form behind a depth + cycle guard.
1943
+ *
1944
+ * @remarks
1945
+ * - **Composes, never re-implements.** Per-phase bounded concurrency is one
1946
+ * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
1947
+ * `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
1948
+ * timeout / budget fold through {@link createAbort} / {@link createTimeout} +
1949
+ * `AbortSignal.any` (exactly as the agent runtime folds its bounds); pacing is the shipped
1950
+ * {@link SchedulerInterface}. The runner writes ZERO concurrency / retry / abort logic of
1951
+ * its own — it only sequences phases, dispatches a task, and drives the live entity.
1952
+ * - **Phases sequential, tasks concurrent.** `#execute` awaits the phases in order (phase
1953
+ * N+1 starts only once phase N has fully settled). Within a phase, ALL its tasks are the
1954
+ * one Runner's `inputs`, run at `concurrency` = the phase's
1955
+ * {@link PhaseDefinition.concurrency} (default {@link DEFAULT_PHASE_CONCURRENCY}).
1956
+ * - **Dispatch by name.** `#dispatch` branches on the task's
1957
+ * {@link import('./types.js').TaskForm} (read from the `definition`, correlated by `id`):
1958
+ * `function` → the {@link WorkflowFunctions} registry, `tool` → the
1959
+ * {@link ToolManagerInterface}, `agent` → the {@link WorkflowAgents} resolver (W-c2). A
1960
+ * handler that is NOT found (an unregistered name for ANY form) AUTO-COMPLETES — the
1961
+ * ROADMAP no-handler rule.
1962
+ * - **`agent` form + depth/cycle guard (W-c2).** An `agent` task resolves its subagent via
1963
+ * `agents`, BINDS a depth/cycle-aware workflow tool onto the subagent's `context.tools`
1964
+ * (the propagation seam), folds the task's cancellation into the agent run (a workflow
1965
+ * cancel `abort`s the subagent), and drives it: success → `complete(result)`, throw →
1966
+ * `fail(error)`. Before running, the guard REJECTS the task into a typed `DEPTH`
1967
+ * {@link WorkflowError} (`fail`) when running it would push the nested chain past
1968
+ * {@link MAX_WORKFLOW_DEPTH}, OR when its target agent is already an ancestor (a cycle).
1969
+ * The rejected task never runs the agent.
1970
+ * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
1971
+ * THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
1972
+ * (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
1973
+ * `#execute` then `skip`s the remaining tasks / phases (the workflow derives `failed`).
1974
+ * Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
1975
+ * the Runner settles every unit (allSettled) and the run finishes (the workflow derives
1976
+ * `completed`, the failure recorded in the result tree).
1977
+ * - **Abort / Timeout / Budget fold.** `#execute` folds the run's external `signal`, a
1978
+ * {@link TimeoutInterface}, and the `@orkestrel/budget` package's `BudgetInterface`'s
1979
+ * `signal` into one `runSignal` (`AbortSignal.any`); a fire aborts the active phase's Runner
1980
+ * (cancelling every in-flight task) and HALTS the run — the remaining tasks / phases `skip`
1981
+ * and the workflow is force-`stop`ped (settles `stopped`). Each task's
1982
+ * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
1983
+ * `runSignal`, so a handler observes either cause directly.
1984
+ * - **Re-entrant-safe.** No shared per-run mutable field: the active-Runner holder is LOCAL to
1985
+ * each `#execute`, so a nested `execute` (the bound workflow tool re-entering this instance
1986
+ * while the outer run is suspended on an `agent` task) cannot clobber the outer run's state.
1987
+ */
1988
+ var WorkflowRunner = class {
1989
+ #functions;
1990
+ #tools;
1991
+ #agents;
1992
+ #scheduler;
1993
+ #workflowTool;
1994
+ constructor(functions, tools, agents, scheduler, workflowTool) {
1995
+ this.#functions = functions;
1996
+ this.#tools = tools;
1997
+ this.#agents = agents;
1998
+ this.#scheduler = scheduler;
1999
+ this.#workflowTool = workflowTool;
2000
+ }
2001
+ execute(definition, options) {
2002
+ const workflow = new Workflow(definitionToSnapshot(definition, options?.bail ?? definition.bail ?? false), options);
2003
+ const depth = options?.depth ?? 0;
2004
+ const ancestry = [...options?.ancestry ?? [], workflowTag(definition.id)];
2005
+ return this.#execute(workflow, definition, options, depth, ancestry);
2006
+ }
2007
+ async #execute(workflow, definition, options, depth, ancestry) {
2008
+ const ms = options?.timeout;
2009
+ const timeout = ms !== void 0 && ms > 0 ? createTimeout({ ms }) : void 0;
2010
+ timeout?.start();
2011
+ options?.budget?.start();
2012
+ const runSignal = this.#fold(options, timeout);
2013
+ const holder = { runner: void 0 };
2014
+ const onCancel = () => holder.runner?.abort(runSignal?.reason);
2015
+ if (runSignal !== void 0) if (runSignal.aborted) onCancel();
2016
+ else runSignal.addEventListener("abort", onCancel, { once: true });
2017
+ try {
2018
+ const phases = workflow.phases.phases();
2019
+ for (let index = 0; index < phases.length; index += 1) {
2020
+ const phase = phases[index];
2021
+ if (phase === void 0) continue;
2022
+ if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2023
+ this.#skipFrom(phases, index);
2024
+ break;
2025
+ }
2026
+ if (await this.#runPhase(workflow, phase, this.#phaseOf(definition, phase.id), runSignal, holder, depth, ancestry)) {
2027
+ this.#skipFrom(phases, index + 1);
2028
+ break;
2029
+ }
2030
+ if (index < phases.length - 1 && !this.#cancelled(runSignal)) try {
2031
+ await this.#scheduler.yield(runSignal === void 0 ? void 0 : { signal: runSignal });
2032
+ } catch {}
2033
+ }
2034
+ if (this.#cancelled(runSignal)) {
2035
+ this.#skipFrom(workflow.phases.phases(), 0);
2036
+ if (this.#stoppable(workflow)) workflow.stop();
2037
+ } else if (this.#completable(workflow)) workflow.complete();
2038
+ return {
2039
+ workflow,
2040
+ status: workflow.status,
2041
+ results: workflow.results()
2042
+ };
2043
+ } finally {
2044
+ timeout?.clear();
2045
+ runSignal?.removeEventListener("abort", onCancel);
2046
+ }
2047
+ }
2048
+ async #runPhase(workflow, phase, definition, runSignal, holder, depth, ancestry) {
2049
+ const tasks = phase.tasks.tasks();
2050
+ if (tasks.length === 0) return false;
2051
+ const bail = definition?.bail ?? workflow.bail;
2052
+ const concurrency = definition?.concurrency !== void 0 && definition.concurrency > 0 ? definition.concurrency : DEFAULT_PHASE_CONCURRENCY;
2053
+ const attempts = /* @__PURE__ */ new Map();
2054
+ const runner = new Runner({
2055
+ concurrency,
2056
+ entries: (task) => {
2057
+ const def = this.#taskOf(definition, task.id);
2058
+ return {
2059
+ retries: def?.retries,
2060
+ timeout: def?.timeout
2061
+ };
2062
+ },
2063
+ handler: (controller) => this.#runTask(workflow, controller.input, this.#taskOf(definition, controller.input.id), controller, runSignal, bail, attempts, depth, ancestry)
2064
+ });
2065
+ holder.runner = runner;
2066
+ try {
2067
+ await runner.execute(tasks);
2068
+ return false;
2069
+ } catch {
2070
+ return !this.#cancelled(runSignal);
2071
+ } finally {
2072
+ runner.destroy();
2073
+ holder.runner = void 0;
2074
+ }
2075
+ }
2076
+ async #runTask(workflow, task, definition, controller, runSignal, bail, attempts, depth, ancestry) {
2077
+ const signal = this.#taskSignal(controller.signal, runSignal);
2078
+ const attempt = (attempts.get(task.id) ?? 0) + 1;
2079
+ attempts.set(task.id, attempt);
2080
+ const last = attempt > Math.max(0, definition?.retries ?? 0);
2081
+ if (task.status === "pending") task.start();
2082
+ if (this.#skipping(controller, runSignal)) {
2083
+ this.#skip(task);
2084
+ return;
2085
+ }
2086
+ const handle = new TaskController(signal, task.snapshot().metadata, task.context, () => workflow.results());
2087
+ try {
2088
+ const value = await this.#dispatch(definition, handle, depth, ancestry);
2089
+ if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2090
+ this.#skip(task);
2091
+ return;
2092
+ }
2093
+ if (signal.aborted) {
2094
+ this.#timedOut(task, last);
2095
+ return;
2096
+ }
2097
+ task.complete(value);
2098
+ } catch (error) {
2099
+ if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2100
+ this.#skip(task);
2101
+ return;
2102
+ }
2103
+ if (signal.aborted) {
2104
+ this.#timedOut(task, last);
2105
+ return;
2106
+ }
2107
+ if (!last) throw error;
2108
+ task.fail(error);
2109
+ if (bail) throw error;
2110
+ }
2111
+ }
2112
+ #timedOut(task, last) {
2113
+ if (!last) return;
2114
+ task.fail(/* @__PURE__ */ new Error(`task '${task.id}' timed out`));
2115
+ }
2116
+ async #dispatch(definition, controller, depth, ancestry) {
2117
+ const form = definition?.run;
2118
+ if (form !== void 0 && isFunctionTask(form)) {
2119
+ const handler = this.#functions[form.name];
2120
+ if (handler !== void 0) return handler(controller);
2121
+ return;
2122
+ }
2123
+ if (form !== void 0 && isToolTask(form)) {
2124
+ const tools = this.#tools;
2125
+ if (tools === void 0) return void 0;
2126
+ if (tools.tool(form.name) === void 0) return void 0;
2127
+ const result = await tools.execute({
2128
+ id: controller.task.id,
2129
+ name: form.name,
2130
+ arguments: controller.input
2131
+ });
2132
+ if (result.error !== void 0) throw new Error(result.error);
2133
+ return result.value;
2134
+ }
2135
+ if (form !== void 0 && isAgentTask(form)) return this.#dispatchAgent(form.name, controller, depth, ancestry);
2136
+ }
2137
+ async #dispatchAgent(name, controller, depth, ancestry) {
2138
+ const resolve = this.#agents;
2139
+ if (resolve === void 0) return void 0;
2140
+ const agent = resolve(name);
2141
+ if (agent === void 0) return void 0;
2142
+ if (depth + 1 > 8) throw new WorkflowError("DEPTH", `agent '${name}' exceeds max workflow depth`, {
2143
+ agent: name,
2144
+ depth,
2145
+ max: 8
2146
+ });
2147
+ const tag = agentTag(name);
2148
+ if (ancestry.includes(tag)) throw new WorkflowError("DEPTH", `agent '${name}' is already an ancestor (cycle)`, {
2149
+ agent: name,
2150
+ ancestry: [...ancestry]
2151
+ });
2152
+ this.#bindWorkflowTool(agent, depth, [...ancestry, tag], controller.task.phase.workflow.id);
2153
+ return this.#runAgent(agent, controller.signal);
2154
+ }
2155
+ #bindWorkflowTool(agent, depth, ancestry, workflowId) {
2156
+ const bind = this.#workflowTool;
2157
+ if (bind === void 0) return;
2158
+ const wrapped = {
2159
+ id: workflowId,
2160
+ name: workflowId,
2161
+ phases: []
2162
+ };
2163
+ agent.context.tools.add(bind(wrapped, this, {
2164
+ depth,
2165
+ ancestry
2166
+ }));
2167
+ }
2168
+ async #runAgent(agent, signal) {
2169
+ const onAbort = () => agent.abort(signal.reason);
2170
+ if (signal.aborted) agent.abort(signal.reason);
2171
+ else signal.addEventListener("abort", onAbort, { once: true });
2172
+ try {
2173
+ return await agent.generate();
2174
+ } finally {
2175
+ signal.removeEventListener("abort", onAbort);
2176
+ }
2177
+ }
2178
+ #taskSignal(unitSignal, runSignal) {
2179
+ if (runSignal === void 0) return unitSignal;
2180
+ return createAbort({ signal: AbortSignal.any([unitSignal, runSignal]) }).signal;
2181
+ }
2182
+ #fold(options, timeout) {
2183
+ const signals = [];
2184
+ if (options?.signal !== void 0) signals.push(options.signal);
2185
+ if (timeout !== void 0) signals.push(timeout.signal);
2186
+ if (options?.budget !== void 0) signals.push(options.budget.signal);
2187
+ if (signals.length === 0) return void 0;
2188
+ if (signals.length === 1) return signals[0];
2189
+ return AbortSignal.any(signals);
2190
+ }
2191
+ #skipFrom(phases, index) {
2192
+ for (let cursor = index; cursor < phases.length; cursor += 1) {
2193
+ const phase = phases[cursor];
2194
+ if (phase === void 0) continue;
2195
+ for (const task of phase.tasks.tasks()) this.#skip(task);
2196
+ }
2197
+ }
2198
+ #skip(task) {
2199
+ if (task.status === "pending" || task.status === "running") task.skip();
2200
+ }
2201
+ #skipping(controller, runSignal) {
2202
+ return controller.aborted || runSignal?.aborted === true;
2203
+ }
2204
+ #cancelled(runSignal) {
2205
+ return runSignal?.aborted === true;
2206
+ }
2207
+ #halted(workflow) {
2208
+ const status = workflow.status;
2209
+ return status === "failed" || status === "skipped" || status === "stopped";
2210
+ }
2211
+ #stoppable(workflow) {
2212
+ const status = workflow.status;
2213
+ return status !== "failed" && status !== "stopped";
2214
+ }
2215
+ #completable(workflow) {
2216
+ return workflow.status === "pending";
2217
+ }
2218
+ #phaseOf(definition, id) {
2219
+ return definition.phases.find((phase) => phase.id === id);
2220
+ }
2221
+ #taskOf(phase, id) {
2222
+ return phase?.tasks.find((task) => task.id === id);
2223
+ }
2224
+ };
2225
+ //#endregion
2226
+ //#region src/core/factories.ts
2227
+ /**
2228
+ * Compile the workflow definition contract — the JSON Schema, guard, parser, and
2229
+ * seeded generator for a {@link WorkflowDefinition}, all derived from one shape and
2230
+ * kept in lockstep.
2231
+ *
2232
+ * @remarks
2233
+ * The returned {@link ContractInterface}:
2234
+ * - `schema` — the emitted JSON Schema for a workflow definition.
2235
+ * - `is` — a total guard that narrows `unknown` to a valid {@link WorkflowDefinition}
2236
+ * (malformed input returns `false`, never throws).
2237
+ * - `parse` — coerces `unknown` to a {@link WorkflowDefinition}, or `undefined` when
2238
+ * it does not match.
2239
+ * - `generate` — produces a deterministic valid {@link WorkflowDefinition} from an
2240
+ * optional seeded random source.
2241
+ *
2242
+ * @returns The compiled {@link WorkflowDefinition} contract
2243
+ *
2244
+ * @example
2245
+ * ```ts
2246
+ * import { createWorkflowContract } from '@src/core'
2247
+ *
2248
+ * const contract = createWorkflowContract()
2249
+ * const definition = contract.generate() // a valid WorkflowDefinition
2250
+ * contract.is(definition) // true
2251
+ * contract.parse({ id: '', phases: [] }) // undefined (malformed)
2252
+ * ```
2253
+ */
2254
+ function createWorkflowContract() {
2255
+ const contract = createContract(workflowShape);
2256
+ return {
2257
+ schema: contract.schema,
2258
+ is: contract.is,
2259
+ generate: (random) => contract.generate(random),
2260
+ parse: (value) => contract.parse(value)
2261
+ };
2262
+ }
2263
+ /**
2264
+ * Compile the LENIENT workflow DRAFT contract — identical to
2265
+ * {@link createWorkflowContract} EXCEPT `id` and `name` are OPTIONAL at all three levels
2266
+ * (workflow / phase / task), so a small model can omit the six identity strings.
2267
+ *
2268
+ * @remarks
2269
+ * The widened authoring surface {@link createWorkflowTool} parses an authored blob through
2270
+ * before {@link import('./helpers.js').completeDraft} fills the missing ids/names. It does
2271
+ * NOT relax the canonical contract — {@link createWorkflowContract} stays byte-for-byte
2272
+ * unchanged and STRICT, and the completed draft is re-validated against THAT strict gate
2273
+ * before running (soundness preserved). A PROVIDED `id` / `name` still carries `minLength: 1`,
2274
+ * so an explicitly-empty `id: ''` is REJECTED (parses to `undefined`), never auto-filled —
2275
+ * keeping "garbage" distinct from "omitted". `run` stays required.
2276
+ *
2277
+ * @returns The compiled {@link WorkflowDraft} contract
2278
+ *
2279
+ * @example
2280
+ * ```ts
2281
+ * import { createWorkflowDraftContract, completeDraft } from '@src/core'
2282
+ *
2283
+ * const draft = createWorkflowDraftContract()
2284
+ * const parsed = draft.parse({ phases: [{ tasks: [{ run: { via: 'function', name: 'f' } }] }] })
2285
+ * const definition = parsed && completeDraft(parsed) // ids/names filled positionally
2286
+ * draft.parse({ id: '', phases: [] }) // undefined — an explicit empty id is rejected
2287
+ * ```
2288
+ */
2289
+ function createWorkflowDraftContract() {
2290
+ const contract = createContract(workflowDraftShape);
2291
+ return {
2292
+ schema: contract.schema,
2293
+ is: contract.is,
2294
+ generate: (random) => contract.generate(random),
2295
+ parse: (value) => contract.parse(value)
2296
+ };
2297
+ }
2298
+ /**
2299
+ * Build the live W-b entity tree from a {@link WorkflowDefinition} — the whole
2300
+ * {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
2301
+ * {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
2302
+ * context, its emitter, and the cascade.
2303
+ *
2304
+ * @remarks
2305
+ * The definition is the DECLARATIVE blueprint; this seeds an initial all-`pending`
2306
+ * {@link WorkflowSnapshot} from it ({@link definitionToSnapshot}) and constructs the live
2307
+ * tree over that one path. The `bail` failure policy resolves to `options.bail`, else the
2308
+ * definition's `bail`, else the graceful {@link import('./constants.js').DEFAULT_BAIL}; it
2309
+ * feeds {@link import('./helpers.js').deriveWorkflowStatus}. Per-phase / per-task initial
2310
+ * listeners + metadata travel through `options.phases[id].on` /
2311
+ * `options.phases[id].tasks[id]` (the AGENTS §8 nested-by-id bag). The W-b tree is the
2312
+ * state machine ONLY — it does not execute tasks (W-c drives the transitions).
2313
+ *
2314
+ * @param definition - The workflow definition to bring to life
2315
+ * @param options - Runtime options (initial listeners, `bail` override, per-node options)
2316
+ * @returns The live {@link WorkflowInterface} root
2317
+ *
2318
+ * @example
2319
+ * ```ts
2320
+ * import { createWorkflow } from '@src/core'
2321
+ *
2322
+ * const workflow = createWorkflow(definition, { on: { complete: () => done() } })
2323
+ * const phase = workflow.phase('phase-build')
2324
+ * phase?.task('task-compile')?.start() // pending → running (cascades up)
2325
+ * ```
2326
+ */
2327
+ function createWorkflow(definition, options) {
2328
+ return new Workflow(definitionToSnapshot(definition, options?.bail ?? definition.bail ?? false), options);
2329
+ }
2330
+ /**
2331
+ * Rebuild an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
2332
+ * inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
2333
+ * + recorded results + positional order + the persisted `#override`.
2334
+ *
2335
+ * @remarks
2336
+ * Round-trip fidelity is paramount: a `snapshot()` → `restoreWorkflow()` reproduces the
2337
+ * same status at every node (each `#override` restored DIRECTLY from the snapshot's own
2338
+ * `override` field, not guessed from a status divergence), the same recorded
2339
+ * {@link import('./types.js').TaskResult}s, and the same positional order (an interior
2340
+ * `skip` / `remove` survives). The snapshot is SELF-CONTAINED — it persists the `bail`
2341
+ * policy it ran under, so the restore re-derives status IDENTICALLY without a silent
2342
+ * default; the snapshot's `bail` is the source of truth, while an explicit `options.bail`
2343
+ * still wins when supplied (to deliberately re-run under a different policy). A structurally
2344
+ * invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
2345
+ * non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
2346
+ *
2347
+ * @param snapshot - The snapshot to restore (carries its own `bail` + `override`)
2348
+ * @param options - Runtime options (initial listeners, an optional `bail` override, per-node options)
2349
+ * @returns The restored live {@link WorkflowInterface} root
2350
+ *
2351
+ * @example
2352
+ * ```ts
2353
+ * import { restoreWorkflow } from '@src/core'
2354
+ *
2355
+ * const restored = restoreWorkflow(workflow.snapshot()) // bail comes from the snapshot
2356
+ * restored.status === workflow.status // true
2357
+ * ```
2358
+ */
2359
+ function restoreWorkflow(snapshot, options) {
2360
+ assertSnapshot(snapshot);
2361
+ return new Workflow(snapshot, options);
2362
+ }
2363
+ /**
2364
+ * Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
2365
+ * on every phase — and that its every node's status (and its `override`, when present) is drawn
2366
+ * from the lifecycle vocabulary, throwing a `RESTORE` {@link WorkflowError} otherwise.
2367
+ *
2368
+ * @remarks
2369
+ * The boundary-narrowing guard (AGENTS §14) for {@link restoreWorkflow}: a snapshot is
2370
+ * untrusted JSON, so a status (or an override) outside
2371
+ * {@link import('./constants.js').WORKFLOW_STATUSES} /
2372
+ * {@link import('./constants.js').PHASE_STATUSES} / {@link import('./constants.js').TASK_STATUSES},
2373
+ * or a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy),
2374
+ * is rejected loudly (naming the offending node) rather than silently producing a broken tree.
2375
+ * The `override` is optional, so it is only checked WHEN present. Structural shape beyond these
2376
+ * fields is the contract's concern; this guards exactly the fields the live state machine reads back.
2377
+ *
2378
+ * @param snapshot - The snapshot to validate
2379
+ */
2380
+ function assertSnapshot(snapshot) {
2381
+ if (typeof snapshot.bail !== "boolean") throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has a non-boolean bail`, {
2382
+ workflow: snapshot.id,
2383
+ bail: snapshot.bail
2384
+ });
2385
+ if (!WORKFLOW_STATUSES.includes(snapshot.status)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid status`, {
2386
+ workflow: snapshot.id,
2387
+ status: snapshot.status
2388
+ });
2389
+ if (snapshot.override !== void 0 && !WORKFLOW_STATUSES.includes(snapshot.override)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid override`, {
2390
+ workflow: snapshot.id,
2391
+ override: snapshot.override
2392
+ });
2393
+ for (const phase of snapshot.phases) {
2394
+ if (typeof phase.bail !== "boolean") throw new WorkflowError("RESTORE", `phase '${phase.id}' has a non-boolean bail`, {
2395
+ phase: phase.id,
2396
+ bail: phase.bail
2397
+ });
2398
+ if (!PHASE_STATUSES.includes(phase.status)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid status`, {
2399
+ phase: phase.id,
2400
+ status: phase.status
2401
+ });
2402
+ if (phase.override !== void 0 && !PHASE_STATUSES.includes(phase.override)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid override`, {
2403
+ phase: phase.id,
2404
+ override: phase.override
2405
+ });
2406
+ for (const task of phase.tasks) if (!TASK_STATUSES.includes(task.status)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid status`, {
2407
+ task: task.id,
2408
+ status: task.status
2409
+ });
2410
+ }
2411
+ }
2412
+ /**
2413
+ * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
2414
+ * {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
2415
+ * backend behind the W-d persistence seam.
2416
+ *
2417
+ * @remarks
2418
+ * The snapshot analogue of the server package's `createMemorySessionStore`
2419
+ * (and the {@link createMemoryQueueStore} family), but LEANER — there is no idle-TTL, so no
2420
+ * options bag (AGENTS §21 minimal): a persisted run-state lives until an explicit `delete`. This is
2421
+ * the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
2422
+ * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
2423
+ * table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
2424
+ * driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
2425
+ * concern: read a snapshot back and rebuild the live tree with {@link restoreWorkflow}.
2426
+ *
2427
+ * @returns A memory-backed {@link WorkflowStoreInterface}
2428
+ *
2429
+ * @example
2430
+ * ```ts
2431
+ * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@src/core'
2432
+ *
2433
+ * const store = createMemoryWorkflowStore()
2434
+ * const workflow = createWorkflow(definition)
2435
+ * await store.set(workflow.snapshot()) // persist the run state
2436
+ * const snapshot = await store.get(definition.id)
2437
+ * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
2438
+ * ```
2439
+ */
2440
+ function createMemoryWorkflowStore() {
2441
+ return new MemoryWorkflowStore();
2442
+ }
2443
+ /**
2444
+ * Create a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
2445
+ * driver-pluggable backing for the W-d persistence seam, the opt-in twin of
2446
+ * {@link createMemoryWorkflowStore}.
2447
+ *
2448
+ * @remarks
2449
+ * Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver, the snapshot
2450
+ * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
2451
+ * `rawShape` (a JSON blob), exactly as {@link createDatabaseQueueStore} stores its `input`. The
2452
+ * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
2453
+ * AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
2454
+ * `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
2455
+ * the opaque column sidesteps it (the column reads back as `unknown`, narrowed on `get` by
2456
+ * {@link import('./helpers.js').isWorkflowSnapshot}). The `driver` DEFAULTS to
2457
+ * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
2458
+ * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
2459
+ * the durability is the driver's job, the store engine is shared. It swaps in behind
2460
+ * {@link WorkflowStoreInterface} WITHOUT touching the runner or the entity tree.
2461
+ *
2462
+ * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
2463
+ * @returns A {@link WorkflowStoreInterface} over the driver
2464
+ *
2465
+ * @example
2466
+ * ```ts
2467
+ * import { createDatabaseWorkflowStore, createMemoryDriver, createWorkflow, restoreWorkflow } from '@src/core'
2468
+ *
2469
+ * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
2470
+ * const workflow = createWorkflow(definition)
2471
+ * await store.set(workflow.snapshot()) // persist the run state (one JSON column)
2472
+ * const snapshot = await store.get(definition.id)
2473
+ * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
2474
+ * ```
2475
+ */
2476
+ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
2477
+ return new DatabaseWorkflowStore(createDatabase({
2478
+ driver,
2479
+ tables: { snapshots: {
2480
+ id: stringShape(),
2481
+ snapshot: rawShape({})
2482
+ } }
2483
+ }).table("snapshots"));
2484
+ }
2485
+ /**
2486
+ * Create a workflow runner — a {@link WorkflowRunnerInterface} that EXECUTES a live W-b
2487
+ * workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent,
2488
+ * each task dispatched BY NAME under the workflow's `bail` policy.
2489
+ *
2490
+ * @remarks
2491
+ * The runner is THIN — it re-implements no concurrency / retry / abort logic. Per-phase
2492
+ * bounded concurrency is one {@link createRunner} per phase;
2493
+ * `bail` maps onto that Runner's fail-fast (`true` — the first failure aborts the in-flight
2494
+ * siblings + skips the rest) vs settle-all (`false` — failures are recorded, the run
2495
+ * finishes); the run-level abort / timeout / budget ({@link import('./types.js').WorkflowRunOptions})
2496
+ * fold through `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped
2497
+ * scheduler. `execute(definition, options?)` BUILDS the live tree from the definition itself
2498
+ * (via {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`),
2499
+ * drives the live entity (`start` → `complete` / `fail`), and resolves a
2500
+ * {@link import('./types.js').WorkflowResult}.
2501
+ *
2502
+ * A task is dispatched on its {@link import('./types.js').TaskForm}: `function` → the
2503
+ * `functions` registry, `tool` → the `tools` {@link ToolManagerInterface}, `agent` → the
2504
+ * `agents` {@link import('./types.js').WorkflowAgents} resolver (W-c2), behind a depth + cycle
2505
+ * guard. A task whose handler is NOT found (an unregistered name for ANY form) AUTO-COMPLETES
2506
+ * (the ROADMAP no-handler rule).
2507
+ *
2508
+ * The runner is constructed with a reference to {@link createWorkflowTool} (the workflow-tool
2509
+ * binder) so it can BIND a depth/cycle-aware workflow tool onto a dispatched subagent's context
2510
+ * — the propagation seam — WITHOUT this module's classes importing its own `factories.ts` (the
2511
+ * factories→classes direction; the binder is injected as a value at construction).
2512
+ *
2513
+ * @param options - The behavior registries (`functions` / `tools` / `agents`) the runner
2514
+ * dispatches a task by name through, plus an optional pacing `scheduler` (default the shipped
2515
+ * cross-environment one). Omitting `functions` / `tools` / `agents` makes those task forms
2516
+ * auto-complete (no handler). See {@link WorkflowRunnerOptions}.
2517
+ * @returns A working {@link WorkflowRunnerInterface}
2518
+ *
2519
+ * @example
2520
+ * ```ts
2521
+ * import { createWorkflowRunner, createToolManager } from '@src/core'
2522
+ *
2523
+ * const tools = createToolManager()
2524
+ * const runner = createWorkflowRunner({
2525
+ * functions: { compile: async (controller) => `built ${controller.task.id}` },
2526
+ * tools,
2527
+ * })
2528
+ * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
2529
+ * { id: 't', name: 'T', run: { via: 'function', name: 'compile' } },
2530
+ * ] }] }
2531
+ * const result = await runner.execute(definition) // builds + drives the tree
2532
+ * result.status // 'completed'
2533
+ * result.workflow.phase('p')?.task('t')?.status // 'completed'
2534
+ * ```
2535
+ */
2536
+ function createWorkflowRunner(options) {
2537
+ return new WorkflowRunner(options?.functions ?? {}, options?.tools, options?.agents, options?.scheduler ?? createScheduler(), createWorkflowTool);
2538
+ }
2539
+ /**
2540
+ * Wrap a {@link WorkflowDefinition} as an LLM-callable {@link ToolInterface} — it ADVERTISES
2541
+ * the SIMPLE flat authoring shape (`{ name?, steps: [{ name, via? }] }`) as its `parameters` so
2542
+ * even a small model can author a complete tree, and its handler EXPANDS / COMPLETES the
2543
+ * authored blob, validates it against the STRICT contract, runs it through `runner`, and
2544
+ * returns the run SUMMARY (throwing a typed {@link WorkflowError} on failure).
2545
+ *
2546
+ * @remarks
2547
+ * A plain {@link ToolManagerInterface}-compatible tool (so `createMCPServer` / `createMCPRoutes`
2548
+ * expose it for free — nothing MCP is wired here). It is ALSO the propagation carrier the
2549
+ * {@link WorkflowRunner} binds onto a dispatched subagent (W-c2): because a tool handler receives
2550
+ * ONLY the model-supplied `args` (no ambient context, no signal), the run's depth + ancestry are
2551
+ * CLOSED OVER at bind time via {@link WorkflowToolOptions}, and the handler runs the nested
2552
+ * workflow at `depth + 1` with the extended ancestry.
2553
+ *
2554
+ * **Widened authoring surface (additive — the canonical contract + runner stay STRICT and
2555
+ * unchanged).** A 2B model reliably CALLS the tool but cannot reliably emit the full four-level
2556
+ * nested {@link WorkflowDefinition} (six required `id`/`name` strings, a nested tagged union,
2557
+ * all-or-nothing). So the tool ACCEPTS three authoring forms and converges them on the SAME
2558
+ * strict {@link createWorkflowContract} gate before running (soundness preserved):
2559
+ * - the FLAT shape `{ name?, steps: [{ name, via? }] }` — the ADVERTISED `parameters` (the simplest
2560
+ * form, {@link import('./helpers.js').expandSteps}'d into one one-task phase per step);
2561
+ * - a nested DRAFT with any `id`/`name` OMITTED — {@link createWorkflowDraftContract}-parsed then
2562
+ * {@link import('./helpers.js').completeDraft}'d (missing ids synthesized positionally);
2563
+ * - the full nested {@link WorkflowDefinition} — the advanced escape-hatch (documented in the
2564
+ * description), accepted as the draft super-set.
2565
+ *
2566
+ * The handler conforms to the universal tool-handler contract (AGENTS §14): it returns the PLAIN
2567
+ * run-summary VALUE on success and THROWS a typed {@link WorkflowError} on every failure path. It
2568
+ * does NOT build a {@link ToolResult} itself — the `@orkestrel/agent` package's `ToolManager`
2569
+ * performs the ONE canonical wrap (`{ id, name, value }` on a return; `{ id, name, error }` on a
2570
+ * throw, ISOLATED so nothing escapes the run), so the outcome appears EXACTLY ONCE, identically,
2571
+ * over BOTH the agent loop and MCP (a throw → MCP `isError: true`):
2572
+ * - **No authored args** (an empty `arguments`) ⇒ runs the WRAPPED `definition`.
2573
+ * - **A `steps` array** ⇒ the FLAT form: parse it, {@link import('./helpers.js').expandSteps} it.
2574
+ * - **Otherwise** ⇒ the nested form: {@link createWorkflowDraftContract}-parse it,
2575
+ * {@link import('./helpers.js').completeDraft} it.
2576
+ * - **Strict gate** ⇒ the expanded / completed result is validated against
2577
+ * {@link createWorkflowContract}.`is`; a blob that can't expand, or whose result fails the strict
2578
+ * gate (e.g. an explicit empty `id`, `concurrency: 0`) ⇒ THROW a `TOOL` {@link WorkflowError} (no run).
2579
+ * - **Over-deep / cyclic** ⇒ THROW a `DEPTH` {@link WorkflowError} when the nested run would exceed
2580
+ * {@link MAX_WORKFLOW_DEPTH}, or the target workflow id is already an ancestor (a cycle) — the
2581
+ * same `code` the agent-task guard raises.
2582
+ * - **Otherwise** ⇒ `runner.execute(target, { depth: depth + 1, ancestry: … })`, RETURNING the
2583
+ * plain summary of the terminal run (`{ status, count }`, via {@link workflowToolSummary}).
2584
+ *
2585
+ * @param definition - The workflow the tool runs when called with no authored args
2586
+ * @param runner - The {@link WorkflowRunnerInterface} that executes the (nested) workflow
2587
+ * @param options - The depth + ancestry to run the nested workflow under (see
2588
+ * {@link WorkflowToolOptions}); omitted ⇒ depth `0` / empty ancestry (a top-level wrap)
2589
+ * @returns A {@link ToolInterface} (named {@link import('./constants.js').WORKFLOW_TOOL_NAME})
2590
+ * whose `parameters` advertise the FLAT authoring schema (the nested form stays accepted)
2591
+ *
2592
+ * @example
2593
+ * ```ts
2594
+ * import { createWorkflowRunner, createWorkflowTool, createToolManager } from '@src/core'
2595
+ *
2596
+ * const runner = createWorkflowRunner()
2597
+ * const tool = createWorkflowTool(definition, runner)
2598
+ * const tools = createToolManager()
2599
+ * tools.add(tool) // a model can now author + run a workflow in one call
2600
+ * ```
2601
+ */
2602
+ function createWorkflowTool(definition, runner, options) {
2603
+ const strict = createWorkflowContract();
2604
+ const draft = createWorkflowDraftContract();
2605
+ const steps = createContract(workflowStepsShape);
2606
+ const depth = options?.depth ?? 0;
2607
+ const ancestry = options?.ancestry ?? [];
2608
+ return createTool({
2609
+ name: WORKFLOW_TOOL_NAME,
2610
+ description: WORKFLOW_TOOL_DESCRIPTION,
2611
+ parameters: schemaToParameters(steps.schema),
2612
+ execute: async (args) => {
2613
+ let target;
2614
+ if (Object.keys(args).length === 0) target = definition;
2615
+ else if (Array.isArray(args.steps)) {
2616
+ const flat = steps.parse(args);
2617
+ target = flat === void 0 ? void 0 : expandSteps(flat);
2618
+ } else {
2619
+ const parsed = draft.parse(args);
2620
+ target = parsed === void 0 ? void 0 : completeDraft(parsed);
2621
+ }
2622
+ if (target === void 0 || !strict.is(target)) throw new WorkflowError("TOOL", "malformed workflow definition", { workflow: definition.id });
2623
+ if (depth + 1 > 8) throw new WorkflowError("DEPTH", `nested workflow exceeds max depth 8`, {
2624
+ workflow: target.id,
2625
+ depth,
2626
+ max: 8
2627
+ });
2628
+ const tag = workflowTag(target.id);
2629
+ if (ancestry.includes(tag)) throw new WorkflowError("DEPTH", `workflow '${target.id}' is already an ancestor (cycle)`, {
2630
+ workflow: target.id,
2631
+ ancestry: [...ancestry]
2632
+ });
2633
+ return workflowToolSummary(await runner.execute(target, {
2634
+ depth: depth + 1,
2635
+ ancestry: [...ancestry, tag]
2636
+ }));
2637
+ }
2638
+ });
2639
+ }
2640
+ /**
2641
+ * Create the safe cross-environment cooperative-yield default — a
2642
+ * {@link SchedulerInterface} built on `setTimeout` / `clearTimeout` alone, so it
2643
+ * runs unchanged in both the browser and Node.
2644
+ *
2645
+ * @remarks
2646
+ * `yield()` gives the host a turn via a zero-delay macrotask (so pending I/O,
2647
+ * timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
2648
+ * after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
2649
+ * with the signal's `reason` on abort (with full timer/listener cleanup).
2650
+ * `options.priority` is accepted for contract compliance but treated uniformly by
2651
+ * this default — environment backends honour it.
2652
+ *
2653
+ * @returns A working {@link SchedulerInterface}
2654
+ *
2655
+ * @example
2656
+ * ```ts
2657
+ * import { createAbort, createScheduler } from '@src/core'
2658
+ *
2659
+ * const abort = createAbort()
2660
+ * const scheduler = createScheduler()
2661
+ *
2662
+ * // A cooperative loop: do a unit of work, then hand the host a turn.
2663
+ * while (!abort.signal.aborted) {
2664
+ * doSomeWork()
2665
+ * await scheduler.yield({ signal: abort.signal })
2666
+ * }
2667
+ * ```
2668
+ *
2669
+ * @example
2670
+ * ```ts
2671
+ * import { createScheduler } from '@src/core'
2672
+ *
2673
+ * // A backoff: wait a growing interval between retries.
2674
+ * const scheduler = createScheduler()
2675
+ * for (let attempt = 0; attempt < 5; attempt += 1) {
2676
+ * if (await tryOnce()) break
2677
+ * await scheduler.delay(2 ** attempt * 100)
2678
+ * }
2679
+ * ```
2680
+ */
2681
+ function createScheduler() {
2682
+ return new Scheduler();
2683
+ }
2684
+ /**
2685
+ * Create a thin generic orchestrator that drives declared units — and any they
2686
+ * `spawn` — through a bounded-concurrency queue, collecting their results in order.
2687
+ *
2688
+ * @remarks
2689
+ * The Runner composes the workers `Queue` for backpressure, FIFO ordering, bounded
2690
+ * concurrency, retries, and the per-attempt timeout — it adds only orchestration, not
2691
+ * a second concurrency engine. `execute(inputs)` runs the unit set ONCE (a second call
2692
+ * throws) and resolves the units' results in order: the declared inputs first, then
2693
+ * any `spawn`ed siblings in spawn order. Each unit's handler gets a `Controller` — its
2694
+ * `id` / `input`, a `signal` that fires on the unit's `abort`, a runner-level `abort`,
2695
+ * or the attempt's timeout, a promise-parked `wait()`, and `spawn(input)` to fan out
2696
+ * sibling units. The run is **fail-fast**: the first unit failure (after retries)
2697
+ * aborts every other unit and rejects `execute` with that error. **Observable (§13):** a
2698
+ * typed `emitter` surfaces `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort`.
2699
+ *
2700
+ * Because `spawn` is fire-and-track (the runner awaits the whole spawn closure via an
2701
+ * outstanding-unit count, not a one-time snapshot), a handler need NOT await its spawns
2702
+ * for them to run — and on a bounded runner it should NOT `await` a spawn inline (a
2703
+ * slot-holding handler awaiting its own spawn can deadlock); fan out and return instead.
2704
+ *
2705
+ * @typeParam TInput - The work input each unit carries
2706
+ * @typeParam TResult - The value a unit's handler resolves
2707
+ * @param options - The `handler` plus optional `concurrency` (default `1`), `retries`
2708
+ * (default `0`), and a default per-attempt `timeout` in milliseconds
2709
+ * @returns A working {@link RunnerInterface}
2710
+ *
2711
+ * @example
2712
+ * ```ts
2713
+ * import { createRunner } from '@src/core'
2714
+ *
2715
+ * // A handler that fans out one sibling per declared unit, then returns its own value.
2716
+ * const runner = createRunner<number, number>({
2717
+ * concurrency: 4,
2718
+ * handler: (controller) => {
2719
+ * if (controller.input < 10) controller.spawn(controller.input + 100) // fire-and-track
2720
+ * return controller.input
2721
+ * },
2722
+ * })
2723
+ *
2724
+ * const results = await runner.execute([1, 2, 3])
2725
+ * // [1, 2, 3, 101, 102, 103] — declared inputs first (in order), then spawns (in order)
2726
+ * ```
2727
+ */
2728
+ function createRunner(options) {
2729
+ return new Runner(options);
2730
+ }
2731
+ //#endregion
2732
+ export { Controller, DEFAULT_BAIL, DEFAULT_PHASE_CONCURRENCY, DatabaseWorkflowStore, MAX_WORKFLOW_DEPTH, MemoryWorkflowStore, PHASE_STATUSES, Phase, PhaseManager, Runner, Scheduler, TASK_STATUSES, TASK_TRANSITIONS, TASK_VIAS, TERMINAL_TASK_STATUSES, Task, TaskController, TaskManager, WORKFLOW_STATUSES, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_FLAT_EXAMPLE, WORKFLOW_TOOL_NAME, WORKFLOW_TOOL_NESTED_EXAMPLE, Workflow, WorkflowError, WorkflowRunner, agentTag, assertSnapshot, buildPhaseContext, buildTaskContext, buildWorkflowContext, canTransitionTask, collectResults, completeDraft, completePhaseDraft, completeTaskDraft, createDatabaseWorkflowStore, createDeferred, createMemoryWorkflowStore, createRunner, createScheduler, createWorkflow, createWorkflowContract, createWorkflowDraftContract, createWorkflowRunner, createWorkflowTool, definitionToSnapshot, derivePhaseStatus, deriveWorkflowStatus, expandSteps, isAgentTask, isFunctionTask, isTerminalStatus, isToolTask, isWorkflowError, isWorkflowSnapshot, phaseDefinitionToSnapshot, phaseDraftShape, phaseShape, restoreWorkflow, stepShape, stepToForm, taskDefinitionToSnapshot, taskDraftShape, taskFormShape, taskShape, workflowDraftShape, workflowShape, workflowStepsShape, workflowTag, workflowToolSummary };
2733
+
2734
+ //# sourceMappingURL=index.js.map