@orkestrel/workflow 0.0.1 → 0.0.2

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.
@@ -1,8 +1,8 @@
1
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";
2
+ import { arrayShape, compileGuard, createContract, integerShape, isArray, isBoolean, isNumber, isRecord, isString, literalShape, objectShape, optionalShape, rawShape, schemaToParameters, stringShape } from "@orkestrel/contract";
3
3
  import { createDatabase, createMemoryDriver } from "@orkestrel/database";
4
- import { Emitter } from "@orkestrel/emitter";
5
4
  import { createAbort } from "@orkestrel/abort";
5
+ import { Emitter } from "@orkestrel/emitter";
6
6
  import { createTimeout } from "@orkestrel/timeout";
7
7
  import { createQueue } from "@orkestrel/queue";
8
8
  //#region src/core/Scheduler.ts
@@ -82,18 +82,6 @@ var Scheduler = class {
82
82
  /** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
83
83
  var DEFAULT_BAIL = false;
84
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
85
  * Every {@link TaskStatus} value, frozen — the lifecycle vocabulary of a task.
98
86
  *
99
87
  * @remarks
@@ -173,63 +161,64 @@ var TASK_TRANSITIONS = Object.freeze({
173
161
  /**
174
162
  * The default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
175
163
  * 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.
164
+ * throttle — a cap that is effectively unbounded for any realistic phase.
177
165
  *
178
166
  * @remarks
179
167
  * The determinism principle fixes that a phase's tasks run CONCURRENTLY; `concurrency` is
180
168
  * 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".
169
+ * all of a phase's tasks at once — modelled as this finite cap so the value flows straight
170
+ * into the substrate {@link import('./types.js').RunnerInterface}'s `concurrency` (which
171
+ * expects a positive integer) without a special unbounded branch. No realistic phase
172
+ * declares enough tasks to reach it, so it behaves as "run them all".
173
+ *
174
+ * WHY `1024` and not a huge sentinel like `1_000_000`: the backing `@orkestrel/queue` Runner
175
+ * EAGERLY spawns one parked worker loop per concurrency unit AT CONSTRUCTION, so this default
176
+ * must be a value whose eager allocation cost is negligible for every default-concurrency
177
+ * phase — a million-unit default meant ~1e6 promise/closure allocations per such phase. A
178
+ * phase may still DECLARE a larger explicit `concurrency` and pays that allocation knowingly.
185
179
  */
186
- var DEFAULT_PHASE_CONCURRENCY = 1e6;
180
+ var DEFAULT_PHASE_CONCURRENCY = 1024;
187
181
  /**
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.
182
+ * The maximum nesting depth a workflow agent workflow chain may reach — the bound
183
+ * the {@link import('./factories.js').createAgentFunction} and
184
+ * {@link import('./factories.js').createWorkflowTool} adapters' depth/cycle guards enforce.
190
185
  *
191
186
  * @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.
187
+ * The limit lives in ONE place. An {@link import('./factories.js').createAgentFunction}-wrapped
188
+ * agent running at this depth can no longer author + run a NESTED workflow through its bound
189
+ * workflow tool (that would be depth `MAX_WORKFLOW_DEPTH + 1`), so the over-deep invocation is
190
+ * REJECTED (a typed `DEPTH` {@link import('./errors.js').WorkflowError} throw). The chain
191
+ * therefore nests workflows down to this depth, and the nested run at
192
+ * depth `MAX_WORKFLOW_DEPTH` fails.
198
193
  */
199
194
  var MAX_WORKFLOW_DEPTH = 8;
200
195
  /**
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).
196
+ * The name under which {@link import('./factories.js').createAgentFunction} BINDS the
197
+ * depth/cycle-aware workflow tool onto a wrapped agent's `context.tools` (`AgentContextInterface`,
198
+ * `@orkestrel/agent`).
204
199
  *
205
200
  * @remarks
206
- * The propagation seam's well-known key: before running an `agent` task, the runner adds a
201
+ * The propagation seam's well-known key: when its `runner` option is supplied, the adapter adds a
207
202
  * {@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.
203
+ * agent's `context.tools`, so it can author + run a NESTED workflow (bounded by
204
+ * {@link MAX_WORKFLOW_DEPTH}). An agent that wants to fan out into a workflow calls this tool by
205
+ * this name; the bound handler runs the nested workflow at depth + 1.
211
206
  */
212
207
  var WORKFLOW_TOOL_NAME = "workflow";
213
208
  /**
214
209
  * 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 }] }`.
210
+ * through {@link import('./factories.js').createWorkflowTool}: `{ name, steps: [{ name }] }`.
216
211
  *
217
212
  * @remarks
218
213
  * 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
214
+ * (not a label) the registry key its task's `run` resolves against. The tool expands this
220
215
  * ({@link import('./helpers.js').expandSteps}) into a valid {@link WorkflowDefinition}. It
221
216
  * is embedded VERBATIM in {@link WORKFLOW_TOOL_DESCRIPTION} and guarded by a parity test
222
217
  * (it must expand to a tree the STRICT contract accepts), so the doc example can never drift.
223
218
  */
224
219
  var WORKFLOW_TOOL_FLAT_EXAMPLE = Object.freeze({
225
220
  name: "release",
226
- steps: Object.freeze([Object.freeze({
227
- name: "compile",
228
- via: "function"
229
- }), Object.freeze({
230
- name: "publish",
231
- via: "tool"
232
- })])
221
+ steps: Object.freeze([Object.freeze({ name: "compile" }), Object.freeze({ name: "publish" })])
233
222
  });
234
223
  /**
235
224
  * A minimal NESTED authoring example — the ADVANCED escape-hatch form a model may use
@@ -249,10 +238,7 @@ var WORKFLOW_TOOL_NESTED_EXAMPLE = Object.freeze({
249
238
  tasks: Object.freeze([Object.freeze({
250
239
  id: "compile",
251
240
  name: "Compile",
252
- run: Object.freeze({
253
- via: "function",
254
- name: "compile"
255
- })
241
+ run: "compile"
256
242
  })])
257
243
  })])
258
244
  });
@@ -261,27 +247,30 @@ var WORKFLOW_TOOL_NESTED_EXAMPLE = Object.freeze({
261
247
  * multi-line guide that teaches a small model how to author a complete workflow tree.
262
248
  *
263
249
  * @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
250
+ * Presents the SIMPLE flat shape (`{ name, steps: [{ name }] }`) as the PRIMARY way with
251
+ * one complete worked example ({@link WORKFLOW_TOOL_FLAT_EXAMPLE}), names that a step's
252
+ * `name` is a REGISTERED name (not a human label), and documents the full nested
267
253
  * {@link WorkflowDefinition} as the ADVANCED form with a minimal example
268
254
  * ({@link WORKFLOW_TOOL_NESTED_EXAMPLE}). Both examples are interpolated VERBATIM from the
269
255
  * validated constants, so a parity test pins them — the description can never drift from a
270
256
  * 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).
257
+ * schema; the nested form is the documented escape-hatch (the tool accepts both). NOTE: a step's
258
+ * "registered behavior name" is authored STRUCTURE only —
259
+ * {@link import('./factories.js').createWorkflowTool} runs the authored tree with no
260
+ * {@link WorkflowFunctions} registry of its own, so every one of its tasks auto-completes under
261
+ * the no-handler rule; the tool validates/synthesizes shape, it does not dispatch behavior.
272
262
  */
273
263
  var WORKFLOW_TOOL_DESCRIPTION = [
274
264
  "Author and run a workflow (phases run sequentially, the tasks within a phase run concurrently) in one call.",
275
265
  "",
276
266
  "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\" }, ... ] }",
267
+ " { \"name\": \"<workflow name>\", \"steps\": [ { \"name\": \"<registered name>\" }, ... ] }",
278
268
  "- 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
269
  "- the top-level \"name\" (the workflow name) is optional. Ids are filled in for you.",
281
270
  "Example:",
282
271
  JSON.stringify(WORKFLOW_TOOL_FLAT_EXAMPLE),
283
272
  "",
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\" }:",
273
+ "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\" (a registered behavior name):",
285
274
  JSON.stringify(WORKFLOW_TOOL_NESTED_EXAMPLE),
286
275
  "In the nested form you may omit any \"id\"/\"name\" and they are filled in positionally; a provided one is kept."
287
276
  ].join("\n");
@@ -332,35 +321,6 @@ function isWorkflowError(value) {
332
321
  //#endregion
333
322
  //#region src/core/helpers.ts
334
323
  /**
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
324
  * The ancestry identifier of a workflow run — `workflow:<id>`.
365
325
  *
366
326
  * @remarks
@@ -477,6 +437,36 @@ function deriveWorkflowStatus(phases) {
477
437
  return "skipped";
478
438
  }
479
439
  /**
440
+ * Derive the PENDING SUFFIX boundary of a positional list of {@link LifecycleStatus}es —
441
+ * the index of the first entry in the contiguous trailing run of `pending` entries.
442
+ *
443
+ * @remarks
444
+ * The native, hook-free replacement for a runner-installed cursor (AGENTS §12): a
445
+ * {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
446
+ * reads this over its live phases' statuses to decide which positions are safe to edit.
447
+ * Because entries run SEQUENTIALLY (phases sequential, AGENTS determinism), every
448
+ * already-started entry forms a contiguous LEADING prefix and every still-`pending`
449
+ * entry forms the trailing suffix — so the boundary is simply the count of leading
450
+ * non-`pending` entries: the index of the first `pending` entry, or the full length when
451
+ * none is `pending` (nothing is safely editable). A `pending` container's entries are ALL
452
+ * `pending`, so the boundary is `0` and every position is naturally accepted — callers
453
+ * need no special case for that.
454
+ *
455
+ * @param statuses - The positional list of statuses to derive the boundary from
456
+ * @returns The index of the first `pending` entry, or `statuses.length` when none is `pending`
457
+ *
458
+ * @example
459
+ * ```ts
460
+ * deriveBoundary(['completed', 'running', 'pending', 'pending']) // 2
461
+ * deriveBoundary(['pending', 'pending']) // 0
462
+ * deriveBoundary(['completed', 'completed']) // 2 (nothing pending)
463
+ * ```
464
+ */
465
+ function deriveBoundary(statuses) {
466
+ const index = statuses.findIndex((status) => status === "pending");
467
+ return index === -1 ? statuses.length : index;
468
+ }
469
+ /**
480
470
  * Test whether the live W-b task state machine may move directly from one
481
471
  * {@link TaskStatus} to another — the legal-transition guard.
482
472
  *
@@ -494,6 +484,66 @@ function canTransitionTask(from, to) {
494
484
  return TASK_TRANSITIONS[from].includes(to);
495
485
  }
496
486
  /**
487
+ * Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
488
+ *
489
+ * @typeParam T - The boxed value's type
490
+ * @param value - The value to box
491
+ * @returns A {@link Success} wrapping `value`
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * const result = success(task) // { success: true, value: task }
496
+ * ```
497
+ */
498
+ function success(value) {
499
+ return {
500
+ success: true,
501
+ value
502
+ };
503
+ }
504
+ /**
505
+ * Box an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
506
+ *
507
+ * @typeParam E - The boxed error's type
508
+ * @param error - The error to box
509
+ * @returns A {@link Failure} wrapping `error`
510
+ *
511
+ * @example
512
+ * ```ts
513
+ * const result = failure(new WorkflowError('MUTATION', 'refused')) // { success: false, error }
514
+ * ```
515
+ */
516
+ function failure(error) {
517
+ return {
518
+ success: false,
519
+ error
520
+ };
521
+ }
522
+ /**
523
+ * Find the first {@link TaskResult} in a positional list whose boxed outcome is a
524
+ * `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
525
+ * `fail`-event lookup.
526
+ *
527
+ * @remarks
528
+ * The shared leaf behind {@link import('./phases/Phase.js').Phase} and
529
+ * {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers ITS tier's
530
+ * results (a phase's own settled tasks, a workflow's flattened `results()`) and feeds
531
+ * them here; the tier-local method keeps the §12 invariant throw (a derived `failed`
532
+ * status guarantees a failing result exists) since throwing on `undefined` is
533
+ * orchestration, not a leaf concern.
534
+ *
535
+ * @param results - The results to scan, in any order
536
+ * @returns The first result whose `result.success` is `false`, or `undefined` if none
537
+ *
538
+ * @example
539
+ * ```ts
540
+ * findFailure([completedResult, failedResult]) // failedResult
541
+ * ```
542
+ */
543
+ function findFailure(results) {
544
+ return results.find((result) => result.result?.success === false);
545
+ }
546
+ /**
497
547
  * Build a {@link WorkflowContext} — the identity every level inherits — from a node's
498
548
  * `id` / `name` / optional `description`.
499
549
  *
@@ -569,10 +619,12 @@ function isWorkflowSnapshot(value) {
569
619
  *
570
620
  * @remarks
571
621
  * 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
622
+ * carry over verbatim, as does each phase's `concurrency` (persisted on the
623
+ * {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `run` /
624
+ * `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
625
+ * so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
626
+ * real work). The `bail` policy carries over — at the
627
+ * workflow tier AND, per phase, the
576
628
  * EFFECTIVE policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
577
629
  * snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped now.
578
630
  * {@link import('./factories.js').createWorkflow} builds from this.
@@ -610,6 +662,7 @@ function definitionToSnapshot(definition, bail) {
610
662
  * The snapshot persists the EFFECTIVE failure policy this phase runs under: the phase's own
611
663
  * `bail` when it declares one, else the `workflowBail` it inherits — so a restore reinstates
612
664
  * the same per-phase policy without a silent default (`effectiveBail = phase.bail ?? workflowBail`).
665
+ * `concurrency` (the resource throttle) carries over verbatim, omitted when undefined.
613
666
  *
614
667
  * @param phase - The phase definition to seed from
615
668
  * @param workflowBail - The workflow-level `bail` default the phase inherits when it declares none
@@ -622,6 +675,7 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
622
675
  ...phase.description === void 0 ? {} : { description: phase.description },
623
676
  status: "pending",
624
677
  bail: phase.bail ?? workflowBail,
678
+ ...phase.concurrency === void 0 ? {} : { concurrency: phase.concurrency },
625
679
  tasks: phase.tasks.map((task) => taskDefinitionToSnapshot(task))
626
680
  };
627
681
  }
@@ -630,6 +684,12 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
630
684
  * {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
631
685
  * result yet, empty metadata).
632
686
  *
687
+ * @remarks
688
+ * `run` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
689
+ * phase's `bail` / `concurrency`) — a restore reinstates the same behavior reference and
690
+ * reliability overrides once paired with a {@link import('./types.js').WorkflowOptions.functions}
691
+ * registry.
692
+ *
633
693
  * @param task - The task definition to seed from
634
694
  * @returns An initial {@link TaskSnapshot}
635
695
  */
@@ -639,7 +699,10 @@ function taskDefinitionToSnapshot(task) {
639
699
  name: task.name,
640
700
  ...task.description === void 0 ? {} : { description: task.description },
641
701
  status: "pending",
642
- metadata: {}
702
+ metadata: {},
703
+ ...task.run === void 0 ? {} : { run: task.run },
704
+ ...task.retries === void 0 ? {} : { retries: task.retries },
705
+ ...task.timeout === void 0 ? {} : { timeout: task.timeout }
643
706
  };
644
707
  }
645
708
  /**
@@ -743,7 +806,7 @@ function completeTaskDraft(task, phaseId, index) {
743
806
  id,
744
807
  name: task.name ?? id,
745
808
  ...task.description === void 0 ? {} : { description: task.description },
746
- run: task.run,
809
+ ...task.run === void 0 ? {} : { run: task.run },
747
810
  ...task.retries === void 0 ? {} : { retries: task.retries },
748
811
  ...task.timeout === void 0 ? {} : { timeout: task.timeout }
749
812
  };
@@ -755,34 +818,81 @@ function completeTaskDraft(task, phaseId, index) {
755
818
  * @remarks
756
819
  * The expansion of the tool's ADVERTISED surface (AGENTS §21 — the simplest form a small
757
820
  * 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? }] }`)
821
+ * the step's `name` becomes the task's `run` (the behavior-registry key). Ids/names are
822
+ * auto-filled positionally it builds an ids-omitted {@link WorkflowDraft} and delegates
823
+ * to {@link completeDraft}, so the two lenient surfaces share ONE synthesis path (step `i`
824
+ * phase `phase-<i>`, its task `phase-<i>-task-0`). The optional `name` becomes the
825
+ * workflow's `name`. The result is a complete definition the caller validates against the
826
+ * STRICT contract before running.
827
+ *
828
+ * @param flat - The flat steps blob (`{ name?, steps: [{ name }] }`)
766
829
  * @returns A complete {@link WorkflowDefinition} (one one-task phase per step)
767
830
  */
768
831
  function expandSteps(flat) {
769
832
  return completeDraft({
770
833
  ...flat.name === void 0 ? {} : { name: flat.name },
771
- phases: flat.steps.map((step) => ({ tasks: [{ run: stepToForm(step) }] }))
834
+ phases: flat.steps.map((step) => ({ tasks: [{ run: step.name }] }))
772
835
  });
773
836
  }
774
837
  /**
775
- * Convert one flat {@link WorkflowStep} into a {@link TaskForm} `name` the form's `name`,
776
- * `via` the form's discriminant (defaulting to `'function'`).
838
+ * Insert one `[key, value]` entry at a positional index into a readonly entries array
839
+ * the pure splice-in step behind an insertion-ordered registry's `add`.
840
+ *
841
+ * @remarks
842
+ * Shared by {@link import('./tasks/TaskManager.js').TaskManager} and
843
+ * {@link import('./phases/PhaseManager.js').PhaseManager}: both convert their
844
+ * insertion-ordered `Map` to `[...map.entries()]`, call this to splice the new entry
845
+ * in at the target index, then rebuild the `Map` from the result (a stateful step that
846
+ * stays a `#` private method — this helper does no `Map` construction). Does not
847
+ * mutate `entries`; returns a new array.
848
+ *
849
+ * @typeParam T - The entry's value type
850
+ * @param entries - The current positional entries, in order
851
+ * @param index - The index to insert at (`0` prepends, `entries.length` appends)
852
+ * @param key - The new entry's key
853
+ * @param value - The new entry's value
854
+ * @returns A new entries array with `[key, value]` inserted at `index`
777
855
  *
778
- * @param step - The flat step
779
- * @returns The {@link TaskForm} the step's task runs
856
+ * @example
857
+ * ```ts
858
+ * insertEntry([['a', 1], ['b', 2]], 1, 'c', 3) // [['a', 1], ['c', 3], ['b', 2]]
859
+ * ```
780
860
  */
781
- function stepToForm(step) {
782
- return {
783
- via: step.via ?? "function",
784
- name: step.name
785
- };
861
+ function insertEntry(entries, index, key, value) {
862
+ const next = [...entries];
863
+ next.splice(index, 0, [key, value]);
864
+ return next;
865
+ }
866
+ /**
867
+ * Reposition the entry keyed `key` to a new positional index in a readonly entries
868
+ * array — the pure remove-then-reinsert step behind an insertion-ordered registry's
869
+ * `move`.
870
+ *
871
+ * @remarks
872
+ * The move counterpart of {@link insertEntry}: finds the entry by `key`, splices it
873
+ * out, then splices it back in at `index`. An absent `key` is a no-op (returns a copy
874
+ * of `entries` unchanged) — the caller (`TaskManager.move` / `PhaseManager.move`)
875
+ * already gates on the target's existence before calling this, so the no-op branch is
876
+ * defensive, never reached in practice. Does not mutate `entries`; returns a new array.
877
+ *
878
+ * @typeParam T - The entry's value type
879
+ * @param entries - The current positional entries, in order
880
+ * @param key - The key of the entry to reposition
881
+ * @param index - The new index for the entry
882
+ * @returns A new entries array with the `key` entry repositioned to `index`
883
+ *
884
+ * @example
885
+ * ```ts
886
+ * moveEntry([['a', 1], ['b', 2], ['c', 3]], 'a', 2) // [['b', 2], ['c', 3], ['a', 1]]
887
+ * ```
888
+ */
889
+ function moveEntry(entries, key, index) {
890
+ const next = [...entries];
891
+ const at = next.findIndex(([entryKey]) => entryKey === key);
892
+ if (at === -1) return next;
893
+ const [entry] = next.splice(at, 1);
894
+ if (entry !== void 0) next.splice(index, 0, entry);
895
+ return next;
786
896
  }
787
897
  /**
788
898
  * Create a {@link DeferredInterface} — a promise whose settlement is driven
@@ -803,41 +913,39 @@ function createDeferred() {
803
913
  reject
804
914
  };
805
915
  }
806
- //#endregion
807
- //#region src/core/shapers.ts
808
916
  /**
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).
917
+ * Park until `signal` aborts — a promise-parked wait (AGENTS §21), never a timer or
918
+ * busy-loop, that NEVER rejects.
813
919
  *
814
920
  * @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`.
921
+ * Resolves IMMEDIATELY when `signal` is already aborted; otherwise attaches a one-shot
922
+ * `abort` listener and resolves when it fires, removing the listener either way. The
923
+ * shared leaf behind the duplicate abort-wiring an execution engine otherwise hand-rolls
924
+ * at every fold point.
925
+ *
926
+ * @param signal - The signal to park on
927
+ * @returns A promise that resolves once `signal` has aborted
928
+ *
929
+ * @example
930
+ * ```ts
931
+ * const controller = new AbortController()
932
+ * const parked = parkSignal(controller.signal)
933
+ * controller.abort()
934
+ * await parked // resolves
935
+ * ```
818
936
  */
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
- }));
937
+ function parkSignal(signal) {
938
+ if (signal.aborted) return Promise.resolve();
939
+ return new Promise((resolve) => {
940
+ signal.addEventListener("abort", () => resolve(), { once: true });
941
+ });
942
+ }
943
+ //#endregion
944
+ //#region src/core/shapers.ts
838
945
  /**
839
- * The shape of a {@link import('./types.js').TaskDefinition} — identity plus the
840
- * behavior reference ({@link taskFormShape}). `description` is optional prose.
946
+ * The shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
947
+ * `run` behavior reference (a plain registry-key string, min length 1). `description` is
948
+ * optional prose.
841
949
  */
842
950
  var taskShape = objectShape({
843
951
  id: stringShape({
@@ -849,7 +957,10 @@ var taskShape = objectShape({
849
957
  description: "Human-readable task name."
850
958
  }),
851
959
  description: optionalShape(stringShape({ description: "Optional task description." })),
852
- run: taskFormShape,
960
+ run: optionalShape(stringShape({
961
+ min: 1,
962
+ description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
963
+ })),
853
964
  retries: optionalShape(integerShape({
854
965
  min: 0,
855
966
  description: "Extra attempts after the first on failure; overrides the phase default. Omitted means none."
@@ -907,7 +1018,7 @@ var workflowShape = objectShape({
907
1018
  * @remarks
908
1019
  * A PROVIDED `id` / `name` still carries `minLength: 1`, so an explicitly-empty `id: ''`
909
1020
  * is INVALID (rejected by the draft contract), never auto-filled — keeping "garbage"
910
- * distinct from "omitted". `run` stays required.
1021
+ * distinct from "omitted". `run` stays optional, mirroring {@link taskShape}.
911
1022
  */
912
1023
  var taskDraftShape = objectShape({
913
1024
  id: optionalShape(stringShape({
@@ -919,7 +1030,10 @@ var taskDraftShape = objectShape({
919
1030
  description: "Task name; defaults to the id when omitted."
920
1031
  })),
921
1032
  description: optionalShape(stringShape({ description: "Optional task description." })),
922
- run: taskFormShape,
1033
+ run: optionalShape(stringShape({
1034
+ min: 1,
1035
+ description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
1036
+ })),
923
1037
  retries: optionalShape(integerShape({
924
1038
  min: 0,
925
1039
  description: "Extra attempts after the first on failure; overrides the phase default. Omitted means none."
@@ -977,33 +1091,25 @@ var workflowDraftShape = objectShape({
977
1091
  bail: optionalShape(literalShape([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
978
1092
  });
979
1093
  /**
980
- * The shape of ONE flat step — `{ name, via? }` — the building block of
1094
+ * The shape of ONE flat step — `{ name }` — the building block of
981
1095
  * {@link workflowStepsShape}.
982
1096
  *
983
1097
  * @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
1098
+ * `name` is the REGISTERED behavior name the step runs (it becomes the task's `run`). The
986
1099
  * tool expands each step into a one-task phase, in order
987
1100
  * ({@link import('./helpers.js').expandSteps}).
988
1101
  */
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
- });
1102
+ var stepShape = objectShape({ name: stringShape({
1103
+ min: 1,
1104
+ description: "The registered behavior name this step runs (becomes the task run)."
1105
+ }) });
1000
1106
  /**
1001
1107
  * The FLAT authoring shape `createWorkflowTool` advertises as its `parameters` — the
1002
- * simplest surface a small model can fill: `{ name?, steps: [{ name, via? }] }`.
1108
+ * simplest surface a small model can fill: `{ name?, steps: [{ name }] }`.
1003
1109
  *
1004
1110
  * @remarks
1005
1111
  * 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
1112
+ * `{ name }`. The tool EXPANDS it ({@link import('./helpers.js').expandSteps}) into a
1007
1113
  * full {@link import('./types.js').WorkflowDefinition} — one one-task phase per step, in
1008
1114
  * order — then validates against the STRICT
1009
1115
  * {@link import('./factories.js').createWorkflowContract} gate. The full nested form is
@@ -1017,6 +1123,43 @@ var workflowStepsShape = objectShape({
1017
1123
  })),
1018
1124
  steps: arrayShape(stepShape, { description: "The ordered steps to run, one after another (each becomes a one-task phase)." })
1019
1125
  });
1126
+ /**
1127
+ * The shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
1128
+ * `pending` task's `name` / `description`, both optional.
1129
+ *
1130
+ * @remarks
1131
+ * Mirrors {@link taskShape}'s `name` / `description` constraints exactly (a provided
1132
+ * `name` still has `minLength: 1`); never `id` / `run` / `retries` / `timeout` (those
1133
+ * are not patchable fields, AGENTS §12).
1134
+ */
1135
+ var taskUpdateShape = objectShape({
1136
+ name: optionalShape(stringShape({
1137
+ min: 1,
1138
+ description: "New task name."
1139
+ })),
1140
+ description: optionalShape(stringShape({ description: "New task description." }))
1141
+ });
1142
+ /**
1143
+ * The shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
1144
+ * `pending` phase's `name` / `description` / `concurrency` / `bail`, all optional.
1145
+ *
1146
+ * @remarks
1147
+ * Mirrors {@link phaseShape}'s corresponding field constraints exactly; never `id` /
1148
+ * `tasks` (structural children change through the phase's own `add` / `remove` /
1149
+ * `move`, not a patch, AGENTS §12).
1150
+ */
1151
+ var phaseUpdateShape = objectShape({
1152
+ name: optionalShape(stringShape({
1153
+ min: 1,
1154
+ description: "New phase name."
1155
+ })),
1156
+ description: optionalShape(stringShape({ description: "New phase description." })),
1157
+ concurrency: optionalShape(integerShape({
1158
+ min: 1,
1159
+ description: "Max tasks in flight at once (a resource throttle); omitted leaves it unchanged."
1160
+ })),
1161
+ bail: optionalShape(literalShape([true, false], { description: "Per-phase failure-policy override; omitted leaves it unchanged." }))
1162
+ });
1020
1163
  //#endregion
1021
1164
  //#region src/core/stores/DatabaseWorkflowStore.ts
1022
1165
  /**
@@ -1177,6 +1320,12 @@ var MemoryWorkflowStore = class {
1177
1320
  * matching event strictly AFTER the state change, BEFORE the cascade; the emitter isolates
1178
1321
  * a listener throw and routes it to its `error` handler (the `error` option), so a buggy
1179
1322
  * observer can never corrupt a transition.
1323
+ * - **Declarative config (AGENTS §12).** `run` / `retries` / `timeout` PERSIST in a
1324
+ * {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
1325
+ * matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
1326
+ * is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
1327
+ * workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
1328
+ * NEVER persisted; `undefined` when `run` is omitted or unregistered (the no-handler rule).
1180
1329
  */
1181
1330
  var Task = class {
1182
1331
  #context;
@@ -1187,7 +1336,13 @@ var Task = class {
1187
1336
  #emitter;
1188
1337
  #status;
1189
1338
  #result;
1190
- constructor(context, phase, workflow, recompute, options, status = "pending", result) {
1339
+ #name;
1340
+ #description;
1341
+ #run;
1342
+ #retries;
1343
+ #timeout;
1344
+ #handler;
1345
+ constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, handler) {
1191
1346
  this.#context = context;
1192
1347
  this.#phase = phase;
1193
1348
  this.#workflow = workflow;
@@ -1199,6 +1354,12 @@ var Task = class {
1199
1354
  });
1200
1355
  this.#status = status;
1201
1356
  this.#result = result;
1357
+ this.#name = context.name;
1358
+ this.#description = context.description;
1359
+ this.#run = run;
1360
+ this.#retries = retries;
1361
+ this.#timeout = timeout;
1362
+ this.#handler = handler;
1202
1363
  }
1203
1364
  get emitter() {
1204
1365
  return this.#emitter;
@@ -1207,10 +1368,10 @@ var Task = class {
1207
1368
  return this.#context.id;
1208
1369
  }
1209
1370
  get name() {
1210
- return this.#context.name;
1371
+ return this.#name;
1211
1372
  }
1212
1373
  get description() {
1213
- return this.#context.description;
1374
+ return this.#description;
1214
1375
  }
1215
1376
  get context() {
1216
1377
  return this.#context;
@@ -1227,6 +1388,18 @@ var Task = class {
1227
1388
  get result() {
1228
1389
  return this.#result;
1229
1390
  }
1391
+ get run() {
1392
+ return this.#run;
1393
+ }
1394
+ get handler() {
1395
+ return this.#handler;
1396
+ }
1397
+ get retries() {
1398
+ return this.#retries;
1399
+ }
1400
+ get timeout() {
1401
+ return this.#timeout;
1402
+ }
1230
1403
  start() {
1231
1404
  this.#transition("running");
1232
1405
  this.#emitter.emit("start", this.id);
@@ -1261,6 +1434,29 @@ var Task = class {
1261
1434
  this.#emitter.emit("stop");
1262
1435
  this.#escalate();
1263
1436
  }
1437
+ /**
1438
+ * Apply a validated declarative patch to SELF (`name` / `description`).
1439
+ *
1440
+ * @remarks
1441
+ * Defense-in-depth (AGENTS §12): the owning
1442
+ * {@link import('../types.js').TaskManagerInterface.update} gates FIRST (target
1443
+ * exists + `pending`), so this is the second, redundant check — it THROWS a
1444
+ * `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
1445
+ *
1446
+ * @param value - The {@link TaskUpdate} fields to apply
1447
+ * @example
1448
+ * ```ts
1449
+ * task.patch({ name: 'Renamed task' })
1450
+ * ```
1451
+ */
1452
+ patch(value) {
1453
+ if (this.#status !== "pending") throw new WorkflowError("MUTATION", `task '${this.id}' cannot be patched while '${this.#status}'`, {
1454
+ task: this.id,
1455
+ status: this.#status
1456
+ });
1457
+ if (value.name !== void 0) this.#name = value.name;
1458
+ if (value.description !== void 0) this.#description = value.description;
1459
+ }
1264
1460
  snapshot() {
1265
1461
  return {
1266
1462
  id: this.id,
@@ -1268,7 +1464,10 @@ var Task = class {
1268
1464
  ...this.description === void 0 ? {} : { description: this.description },
1269
1465
  status: this.#status,
1270
1466
  ...this.#result === void 0 ? {} : { result: this.#result },
1271
- metadata: this.#metadata
1467
+ metadata: this.#metadata,
1468
+ ...this.#run === void 0 ? {} : { run: this.#run },
1469
+ ...this.#retries === void 0 ? {} : { retries: this.#retries },
1470
+ ...this.#timeout === void 0 ? {} : { timeout: this.#timeout }
1272
1471
  };
1273
1472
  }
1274
1473
  #transition(to) {
@@ -1308,6 +1507,11 @@ var Task = class {
1308
1507
  * `tasks()` lists them in positional order, `count` is the size. A `skip` is a STATUS
1309
1508
  * change on a stored task (never a removal), so order survives it; a snapshot RESTORE
1310
1509
  * re-`append`s in the snapshot's order, reproducing it exactly.
1510
+ * - **Gated mutation API (AGENTS §12).** `add` / `remove` / `move` / `update` are the
1511
+ * graceful `Result` counterparts to `append`, gating ONLY on the target's OWN
1512
+ * existence/status/id/bounds — a duplicate id, an absent/non-`pending` target, an
1513
+ * out-of-bounds `index`, or a patch that fails {@link taskUpdateShape} validation all
1514
+ * fail gracefully with a `MUTATION` {@link WorkflowError} instead of throwing.
1311
1515
  * - **No batch matrix.** A phase's tasks are a fixed positional set, so AGENTS §9.2 (the
1312
1516
  * bulk verb overloads) is deliberately omitted — there is no `remove` family here.
1313
1517
  * - **Event-free.** A purely structural container — the live {@link TaskInterface}s own
@@ -1323,18 +1527,51 @@ var Task = class {
1323
1527
  */
1324
1528
  var TaskManager = class {
1325
1529
  #tasks = /* @__PURE__ */ new Map();
1530
+ #isUpdate = compileGuard(taskUpdateShape);
1326
1531
  get count() {
1327
1532
  return this.#tasks.size;
1328
1533
  }
1329
1534
  append(task) {
1535
+ if (this.#tasks.has(task.id)) throw new WorkflowError("MUTATION", `duplicate task id '${task.id}'`, { id: task.id });
1330
1536
  this.#tasks.set(task.id, task);
1331
1537
  }
1538
+ add(task, index) {
1539
+ if (this.#tasks.has(task.id)) return failure(new WorkflowError("MUTATION", `duplicate task id '${task.id}'`, { id: task.id }));
1540
+ const at = index ?? this.#tasks.size;
1541
+ if (at < 0 || at > this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1542
+ this.#reorder(insertEntry([...this.#tasks.entries()], at, task.id, task));
1543
+ return success(task);
1544
+ }
1545
+ remove(id) {
1546
+ const target = this.#tasks.get(id);
1547
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1548
+ this.#tasks.delete(id);
1549
+ return success(target);
1550
+ }
1551
+ move(id, index) {
1552
+ const target = this.#tasks.get(id);
1553
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1554
+ if (index < 0 || index >= this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1555
+ this.#reorder(moveEntry([...this.#tasks.entries()], id, index));
1556
+ return success(target);
1557
+ }
1558
+ update(id, patch) {
1559
+ const target = this.#tasks.get(id);
1560
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1561
+ if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for task '${id}'`, { id }));
1562
+ target.patch(patch);
1563
+ return success(target);
1564
+ }
1332
1565
  task(id) {
1333
1566
  return this.#tasks.get(id);
1334
1567
  }
1335
1568
  tasks() {
1336
1569
  return [...this.#tasks.values()];
1337
1570
  }
1571
+ #reorder(entries) {
1572
+ this.#tasks.clear();
1573
+ for (const [key, value] of entries) this.#tasks.set(key, value);
1574
+ }
1338
1575
  };
1339
1576
  //#endregion
1340
1577
  //#region src/core/phases/Phase.ts
@@ -1359,29 +1596,59 @@ var TaskManager = class {
1359
1596
  * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE, strictly AFTER the
1360
1597
  * recompute + escalate; the emitter isolates a listener throw and routes it to its `error`
1361
1598
  * handler (the `error` option); `fail` carries the failing task's {@link TaskResult}.
1599
+ * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1600
+ * delegating to {@link tasks} (the manager gates the target's own existence/status/id/
1601
+ * bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
1602
+ * gating, purely from this phase's own derived `status` (no runner-installed hook): while
1603
+ * `pending`, any valid `index` is accepted; while `running`, `add` accepts ONLY a pure
1604
+ * append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
1605
+ * `update` always fail gracefully (the tasks are already handed to the execution
1606
+ * substrate); while terminal, everything is refused.
1607
+ * - **Patch (AGENTS §12).** `patch` applies a validated {@link PhaseUpdate} to SELF
1608
+ * (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
1609
+ * `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
1610
+ * the owning {@link WorkflowInterface.update}'s gate.
1611
+ * - **Minting (AGENTS §7).** {@link add} MINTS a live {@link Task} from a {@link TaskDefinition}
1612
+ * (converts it to a {@link TaskSnapshot}, builds the task wired to THIS phase) — the same
1613
+ * construction path {@link #append} uses at build time, so a live mint and a restored/built
1614
+ * task are wired IDENTICALLY. At construction, the workflow-level
1615
+ * {@link import('../types.js').WorkflowFunctions} registry (threaded from
1616
+ * {@link import('../types.js').WorkflowOptions.functions}) resolves each task's `run` name into
1617
+ * its runtime {@link import('../types.js').TaskInterface.handler} ONCE; a `run` that is omitted
1618
+ * or unregistered resolves to no handler (the no-handler rule).
1619
+ * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
1620
+ * quartet, scoped to this phase — a driving
1621
+ * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
1622
+ * pre-dispatch on the workflow's gate FIRST, then this phase's gate, WITHOUT touching
1623
+ * {@link status} — `paused` is runtime-only, never persisted. `skip` / `stop` (this phase's
1624
+ * own terminal forcing) always release a parked {@link wait} waiter, mirroring
1625
+ * {@link import('../Workflow.js').Workflow.destroy}'s cascade — a permanently-ended phase
1626
+ * has nothing left to pause for.
1362
1627
  */
1363
1628
  var Phase = class {
1364
- #context;
1629
+ #id;
1630
+ #name;
1631
+ #description;
1365
1632
  #workflow;
1366
1633
  #escalateUp;
1367
1634
  #tasks = new TaskManager();
1635
+ #functions;
1368
1636
  #bail;
1637
+ #concurrency;
1369
1638
  #emitter;
1370
1639
  #status;
1371
1640
  #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
- };
1641
+ #paused;
1642
+ #gate;
1643
+ constructor(snapshot, workflow, escalate, options, bail, functions) {
1644
+ this.#id = snapshot.id;
1645
+ this.#name = snapshot.name;
1646
+ this.#description = snapshot.description;
1382
1647
  this.#workflow = workflow;
1383
1648
  this.#escalateUp = escalate;
1649
+ this.#functions = functions;
1384
1650
  this.#bail = bail ?? snapshot.bail;
1651
+ this.#concurrency = snapshot.concurrency;
1385
1652
  this.#emitter = new Emitter({
1386
1653
  on: options?.on,
1387
1654
  error: options?.error
@@ -1389,21 +1656,27 @@ var Phase = class {
1389
1656
  for (const task of snapshot.tasks) this.#append(task, options);
1390
1657
  this.#override = snapshot.override;
1391
1658
  this.#status = this.status;
1659
+ this.#paused = false;
1660
+ this.#gate = void 0;
1392
1661
  }
1393
1662
  get emitter() {
1394
1663
  return this.#emitter;
1395
1664
  }
1396
1665
  get id() {
1397
- return this.#context.id;
1666
+ return this.#id;
1398
1667
  }
1399
1668
  get name() {
1400
- return this.#context.name;
1669
+ return this.#name;
1401
1670
  }
1402
1671
  get description() {
1403
- return this.#context.description;
1672
+ return this.#description;
1404
1673
  }
1405
1674
  get context() {
1406
- return this.#context;
1675
+ return buildPhaseContext(this.#workflow.context, {
1676
+ id: this.#id,
1677
+ name: this.#name,
1678
+ ...this.#description === void 0 ? {} : { description: this.#description }
1679
+ });
1407
1680
  }
1408
1681
  get workflow() {
1409
1682
  return this.#workflow;
@@ -1411,6 +1684,12 @@ var Phase = class {
1411
1684
  get bail() {
1412
1685
  return this.#bail;
1413
1686
  }
1687
+ get concurrency() {
1688
+ return this.#concurrency;
1689
+ }
1690
+ get paused() {
1691
+ return this.#paused;
1692
+ }
1414
1693
  get status() {
1415
1694
  return this.#override ?? derivePhaseStatus(this.#statuses());
1416
1695
  }
@@ -1426,10 +1705,81 @@ var Phase = class {
1426
1705
  return results;
1427
1706
  }
1428
1707
  skip() {
1429
- this.#force("skipped");
1708
+ if (!isTerminalStatus(this.status)) this.#force("skipped");
1709
+ this.#paused = false;
1710
+ this.#release();
1430
1711
  }
1431
1712
  stop() {
1432
- this.#force("stopped");
1713
+ if (!isTerminalStatus(this.status)) this.#force("stopped");
1714
+ this.#paused = false;
1715
+ this.#release();
1716
+ }
1717
+ pause() {
1718
+ if (this.#paused || isTerminalStatus(this.status)) return;
1719
+ this.#paused = true;
1720
+ this.#gate = createDeferred();
1721
+ }
1722
+ resume() {
1723
+ if (!this.#paused) return;
1724
+ this.#paused = false;
1725
+ this.#release();
1726
+ }
1727
+ wait() {
1728
+ return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1729
+ }
1730
+ add(definition, index) {
1731
+ const status = this.status;
1732
+ if (isTerminalStatus(status)) return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is terminal`, {
1733
+ id: this.#id,
1734
+ status
1735
+ }));
1736
+ const created = this.#mint(definition);
1737
+ if (status === "running") {
1738
+ const at = index ?? this.#tasks.count;
1739
+ if (at !== this.#tasks.count) return failure(new WorkflowError("MUTATION", `phase '${this.#id}' only accepts an append while executing`, {
1740
+ id: this.#id,
1741
+ index: at
1742
+ }));
1743
+ return this.#addTo(created, index, at);
1744
+ }
1745
+ return this.#addTo(created, index, index ?? this.#tasks.count);
1746
+ }
1747
+ remove(id) {
1748
+ if (this.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is not pending`, {
1749
+ id: this.#id,
1750
+ status: this.status
1751
+ }));
1752
+ const result = this.#tasks.remove(id);
1753
+ if (result.success) this.#emitter.emit("remove", result.value);
1754
+ return result;
1755
+ }
1756
+ move(id, index) {
1757
+ if (this.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is not pending`, {
1758
+ id: this.#id,
1759
+ status: this.status
1760
+ }));
1761
+ const result = this.#tasks.move(id, index);
1762
+ if (result.success) this.#emitter.emit("move", result.value, index);
1763
+ return result;
1764
+ }
1765
+ update(id, patch) {
1766
+ if (this.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is not pending`, {
1767
+ id: this.#id,
1768
+ status: this.status
1769
+ }));
1770
+ const result = this.#tasks.update(id, patch);
1771
+ if (result.success) this.#emitter.emit("update", result.value);
1772
+ return result;
1773
+ }
1774
+ patch(value) {
1775
+ if (this.status !== "pending") throw new WorkflowError("MUTATION", `phase '${this.#id}' can only be patched while pending`, {
1776
+ id: this.#id,
1777
+ status: this.status
1778
+ });
1779
+ if (value.name !== void 0) this.#name = value.name;
1780
+ if (value.description !== void 0) this.#description = value.description;
1781
+ if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
1782
+ if (value.bail !== void 0) this.#bail = value.bail;
1433
1783
  }
1434
1784
  snapshot() {
1435
1785
  return {
@@ -1439,6 +1789,7 @@ var Phase = class {
1439
1789
  status: this.status,
1440
1790
  ...this.#override === void 0 ? {} : { override: this.#override },
1441
1791
  bail: this.#bail,
1792
+ ...this.#concurrency === void 0 ? {} : { concurrency: this.#concurrency },
1442
1793
  tasks: this.#tasks.tasks().map((task) => task.snapshot())
1443
1794
  };
1444
1795
  }
@@ -1463,16 +1814,32 @@ var Phase = class {
1463
1814
  else if (status === "stopped") this.#emitter.emit("stop");
1464
1815
  }
1465
1816
  #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`);
1817
+ const found = findFailure(this.results());
1818
+ if (found === void 0) throw new Error(`phase '${this.id}' derived failed with no failing task result`);
1819
+ return found;
1820
+ }
1821
+ #release() {
1822
+ if (this.#gate === void 0) return;
1823
+ this.#gate.resolve();
1824
+ this.#gate = void 0;
1825
+ }
1826
+ #addTo(task, index, at) {
1827
+ const result = this.#tasks.add(task, index);
1828
+ if (result.success) this.#emitter.emit("add", result.value, at);
1829
+ return result;
1471
1830
  }
1472
1831
  #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);
1832
+ const created = this.#create(task, options?.tasks?.[task.id]);
1474
1833
  this.#tasks.append(created);
1475
1834
  }
1835
+ #create(snapshot, options) {
1836
+ const context = buildTaskContext(this.context, snapshot);
1837
+ const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
1838
+ return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, handler);
1839
+ }
1840
+ #mint(definition) {
1841
+ return this.#create(taskDefinitionToSnapshot(definition), void 0);
1842
+ }
1476
1843
  #statuses() {
1477
1844
  return this.#tasks.tasks().map((task) => task.status);
1478
1845
  }
@@ -1489,6 +1856,11 @@ var Phase = class {
1489
1856
  * `append` adds one at the end, `phase(id)` looks one up, `phases()` lists them in
1490
1857
  * positional order, `count` is the size. A snapshot RESTORE re-`append`s in the
1491
1858
  * snapshot's order, reproducing it exactly.
1859
+ * - **Gated mutation API (AGENTS §12).** `add` / `remove` / `move` / `update` are the
1860
+ * graceful `Result` counterparts to `append`, gating ONLY on the target's OWN
1861
+ * existence/status/id/bounds — a duplicate id, an absent/non-`pending` target, an
1862
+ * out-of-bounds `index`, or a patch that fails {@link phaseUpdateShape} validation
1863
+ * all fail gracefully with a `MUTATION` {@link WorkflowError} instead of throwing.
1492
1864
  * - **No batch matrix.** A workflow's phases are a fixed positional set, so AGENTS §9.2
1493
1865
  * is deliberately omitted.
1494
1866
  * - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own
@@ -1504,18 +1876,51 @@ var Phase = class {
1504
1876
  */
1505
1877
  var PhaseManager = class {
1506
1878
  #phases = /* @__PURE__ */ new Map();
1879
+ #isUpdate = compileGuard(phaseUpdateShape);
1507
1880
  get count() {
1508
1881
  return this.#phases.size;
1509
1882
  }
1510
1883
  append(phase) {
1884
+ if (this.#phases.has(phase.id)) throw new WorkflowError("MUTATION", `duplicate phase id '${phase.id}'`, { id: phase.id });
1511
1885
  this.#phases.set(phase.id, phase);
1512
1886
  }
1887
+ add(phase, index) {
1888
+ if (this.#phases.has(phase.id)) return failure(new WorkflowError("MUTATION", `duplicate phase id '${phase.id}'`, { id: phase.id }));
1889
+ const at = index ?? this.#phases.size;
1890
+ if (at < 0 || at > this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1891
+ this.#reorder(insertEntry([...this.#phases.entries()], at, phase.id, phase));
1892
+ return success(phase);
1893
+ }
1894
+ remove(id) {
1895
+ const target = this.#phases.get(id);
1896
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
1897
+ this.#phases.delete(id);
1898
+ return success(target);
1899
+ }
1900
+ move(id, index) {
1901
+ const target = this.#phases.get(id);
1902
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
1903
+ if (index < 0 || index >= this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1904
+ this.#reorder(moveEntry([...this.#phases.entries()], id, index));
1905
+ return success(target);
1906
+ }
1907
+ update(id, patch) {
1908
+ const target = this.#phases.get(id);
1909
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
1910
+ if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for phase '${id}'`, { id }));
1911
+ target.patch(patch);
1912
+ return success(target);
1913
+ }
1513
1914
  phase(id) {
1514
1915
  return this.#phases.get(id);
1515
1916
  }
1516
1917
  phases() {
1517
1918
  return [...this.#phases.values()];
1518
1919
  }
1920
+ #reorder(entries) {
1921
+ this.#phases.clear();
1922
+ for (const [key, value] of entries) this.#phases.set(key, value);
1923
+ }
1519
1924
  };
1520
1925
  //#endregion
1521
1926
  //#region src/core/Workflow.ts
@@ -1546,27 +1951,52 @@ var PhaseManager = class {
1546
1951
  * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE; the emitter isolates a
1547
1952
  * listener throw and routes it to its `error` handler (the `error` option); `fail` carries
1548
1953
  * the failing task's {@link TaskResult}.
1954
+ * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1955
+ * delegating to {@link phases} (the manager gates the target's own existence/status/id/
1956
+ * bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
1957
+ * bottom-up gating (no runner-installed hook): refused outright while this workflow's own
1958
+ * `status` is terminal; otherwise a target position must fall within the PENDING SUFFIX —
1959
+ * the contiguous trailing run of `pending` phases — whose boundary is
1960
+ * {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
1961
+ * workflow's phases are all `pending`, so the boundary is `0` and every position is
1962
+ * naturally accepted.
1963
+ * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
1964
+ * phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
1965
+ * persisted. `destroy` is a terminal teardown: it aborts {@link signal}, `stop`s every
1966
+ * non-terminal live phase (so an engine parked on a phase's own gate unparks and the tree
1967
+ * lands coherent), forces the `stop` override on THIS workflow when not already terminal,
1968
+ * releases any parked {@link wait} waiter, and marks {@link destroyed} — all four idempotent.
1549
1969
  */
1550
1970
  var Workflow = class {
1551
1971
  #context;
1552
1972
  #bail;
1553
1973
  #bailOverride;
1974
+ #functions;
1554
1975
  #phases = new PhaseManager();
1555
1976
  #emitter;
1556
1977
  #created;
1557
1978
  #updated;
1558
1979
  #status;
1559
1980
  #override;
1981
+ #abort;
1982
+ #paused;
1983
+ #gate;
1984
+ #destroyed;
1560
1985
  constructor(snapshot, options) {
1561
1986
  this.#context = buildWorkflowContext(snapshot);
1562
1987
  this.#bail = options?.bail ?? snapshot.bail;
1563
1988
  this.#bailOverride = options?.bail;
1989
+ this.#functions = options?.functions;
1564
1990
  this.#emitter = new Emitter({
1565
1991
  on: options?.on,
1566
1992
  error: options?.error
1567
1993
  });
1568
1994
  this.#created = snapshot.created;
1569
1995
  this.#updated = snapshot.updated;
1996
+ this.#abort = createAbort();
1997
+ this.#paused = false;
1998
+ this.#gate = void 0;
1999
+ this.#destroyed = false;
1570
2000
  for (const phase of snapshot.phases) this.#append(phase, options);
1571
2001
  this.#override = snapshot.override;
1572
2002
  this.#status = this.status;
@@ -1589,6 +2019,15 @@ var Workflow = class {
1589
2019
  get bail() {
1590
2020
  return this.#bail;
1591
2021
  }
2022
+ get paused() {
2023
+ return this.#paused;
2024
+ }
2025
+ get destroyed() {
2026
+ return this.#destroyed;
2027
+ }
2028
+ get signal() {
2029
+ return this.#abort.signal;
2030
+ }
1592
2031
  get status() {
1593
2032
  return this.#override ?? deriveWorkflowStatus(this.#statuses());
1594
2033
  }
@@ -1602,13 +2041,95 @@ var Workflow = class {
1602
2041
  return collectResults(this.#phases.phases().map((phase) => phase.results()));
1603
2042
  }
1604
2043
  skip() {
1605
- this.#force("skipped");
2044
+ if (!isTerminalStatus(this.status)) this.#force("skipped");
2045
+ this.#paused = false;
2046
+ this.#release();
1606
2047
  }
1607
2048
  stop() {
1608
- this.#force("stopped");
2049
+ if (!isTerminalStatus(this.status)) this.#force("stopped");
2050
+ this.#paused = false;
2051
+ this.#release();
1609
2052
  }
1610
2053
  complete() {
1611
- this.#force("completed");
2054
+ if (this.status === "pending") this.#force("completed");
2055
+ }
2056
+ pause() {
2057
+ if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
2058
+ this.#paused = true;
2059
+ this.#gate = createDeferred();
2060
+ }
2061
+ resume() {
2062
+ if (!this.#paused) return;
2063
+ this.#paused = false;
2064
+ this.#release();
2065
+ }
2066
+ destroy() {
2067
+ if (this.#destroyed) return;
2068
+ this.#destroyed = true;
2069
+ this.#abort.abort();
2070
+ for (const phase of this.#phases.phases()) if (!isTerminalStatus(phase.status)) phase.stop();
2071
+ if (!isTerminalStatus(this.status)) this.stop();
2072
+ this.#paused = false;
2073
+ this.#release();
2074
+ }
2075
+ wait() {
2076
+ return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
2077
+ }
2078
+ add(definition, index) {
2079
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
2080
+ id: this.id,
2081
+ status: this.status
2082
+ }));
2083
+ const at = index ?? this.#phases.count;
2084
+ if (at < this.#boundary()) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' add index precedes boundary`, {
2085
+ id: this.id,
2086
+ index: at
2087
+ }));
2088
+ return this.#addTo(this.#mint(definition), index, at);
2089
+ }
2090
+ remove(id) {
2091
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
2092
+ id: this.id,
2093
+ status: this.status
2094
+ }));
2095
+ const at = this.#indexOf(id);
2096
+ if (at === -1 || at < this.#boundary()) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' cannot remove '${id}'`, {
2097
+ id: this.id,
2098
+ phase: id
2099
+ }));
2100
+ const result = this.#phases.remove(id);
2101
+ if (result.success) this.#emitter.emit("remove", result.value);
2102
+ return result;
2103
+ }
2104
+ move(id, index) {
2105
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
2106
+ id: this.id,
2107
+ status: this.status
2108
+ }));
2109
+ const at = this.#indexOf(id);
2110
+ const boundary = this.#boundary();
2111
+ if (at === -1 || at < boundary || index < boundary) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' cannot move '${id}'`, {
2112
+ id: this.id,
2113
+ phase: id,
2114
+ index
2115
+ }));
2116
+ const result = this.#phases.move(id, index);
2117
+ if (result.success) this.#emitter.emit("move", result.value, index);
2118
+ return result;
2119
+ }
2120
+ update(id, patch) {
2121
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
2122
+ id: this.id,
2123
+ status: this.status
2124
+ }));
2125
+ const at = this.#indexOf(id);
2126
+ if (at === -1 || at < this.#boundary()) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' cannot update '${id}'`, {
2127
+ id: this.id,
2128
+ phase: id
2129
+ }));
2130
+ const result = this.#phases.update(id, patch);
2131
+ if (result.success) this.#emitter.emit("update", result.value);
2132
+ return result;
1612
2133
  }
1613
2134
  snapshot() {
1614
2135
  return {
@@ -1640,14 +2161,34 @@ var Workflow = class {
1640
2161
  else if (status === "failed") this.#emitter.emit("fail", this.#failure());
1641
2162
  else if (status === "stopped") this.#emitter.emit("stop");
1642
2163
  }
2164
+ #addTo(phase, index, at) {
2165
+ const result = this.#phases.add(phase, index);
2166
+ if (result.success) this.#emitter.emit("add", result.value, at);
2167
+ return result;
2168
+ }
2169
+ #indexOf(id) {
2170
+ return this.#phases.phases().findIndex((phase) => phase.id === id);
2171
+ }
2172
+ #boundary() {
2173
+ return deriveBoundary(this.#phases.phases().map((phase) => phase.status));
2174
+ }
1643
2175
  #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`);
2176
+ const found = findFailure(this.results());
2177
+ if (found === void 0) throw new Error(`workflow '${this.id}' derived failed with no failing task result`);
2178
+ return found;
1646
2179
  }
1647
2180
  #append(phase, options) {
1648
- const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride);
2181
+ const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride, this.#functions);
1649
2182
  this.#phases.append(created);
1650
2183
  }
2184
+ #mint(definition) {
2185
+ return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions);
2186
+ }
2187
+ #release() {
2188
+ if (this.#gate === void 0) return;
2189
+ this.#gate.resolve();
2190
+ this.#gate = void 0;
2191
+ }
1651
2192
  #statuses() {
1652
2193
  return this.#phases.phases().map((phase) => ({
1653
2194
  status: phase.status,
@@ -1700,10 +2241,7 @@ var Controller = class {
1700
2241
  return this.#abort.aborted;
1701
2242
  }
1702
2243
  wait() {
1703
- if (this.signal.aborted) return Promise.resolve();
1704
- return new Promise((resolve) => {
1705
- this.signal.addEventListener("abort", () => resolve(), { once: true });
1706
- });
2244
+ return parkSignal(this.signal);
1707
2245
  }
1708
2246
  spawn(input) {
1709
2247
  return this.#spawn(input);
@@ -1748,6 +2286,15 @@ var Controller = class {
1748
2286
  * unit failure (after its retries) records the error and `abort()`s the run, so every
1749
2287
  * sibling's signal fires; later failures are ignored and `execute` rejects with the
1750
2288
  * first error. A user `abort(reason)` likewise rejects a running `execute`.
2289
+ * - **`pause` / `resume` / `stop` (§10) ride the backing Queue.** `pause` / `resume`
2290
+ * delegate straight to the Queue's own pause/resume (holding/releasing the NEXT
2291
+ * dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
2292
+ * GRACEFUL permanent end, distinct from `abort`: still-pending (never-dispatched)
2293
+ * units are rejected by the Queue's own stop WITHOUT their handler ever running, and
2294
+ * `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
2295
+ * not a failure, never tripping fail-fast — while an in-flight unit still runs to
2296
+ * completion and settles normally. `execute` RESOLVES (never rejects) once every unit
2297
+ * has settled, with whatever results actually completed.
1751
2298
  * - **Observable (§13).** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
1752
2299
  * lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
1753
2300
  * fire-and-forget observers. Every event is emitted directly, strictly AFTER the relevant
@@ -1765,11 +2312,13 @@ var Runner = class {
1765
2312
  #aborts = /* @__PURE__ */ new Map();
1766
2313
  #order = [];
1767
2314
  #values = /* @__PURE__ */ new Map();
2315
+ #dispatched = /* @__PURE__ */ new Set();
1768
2316
  #count = 0;
1769
2317
  #drained;
1770
2318
  #started = false;
1771
2319
  #running = false;
1772
2320
  #stopped = false;
2321
+ #stopping = false;
1773
2322
  #failure;
1774
2323
  constructor(options) {
1775
2324
  this.#handler = options.handler;
@@ -1794,6 +2343,39 @@ var Runner = class {
1794
2343
  get stopped() {
1795
2344
  return this.#stopped;
1796
2345
  }
2346
+ get paused() {
2347
+ return this.#queue.paused;
2348
+ }
2349
+ /**
2350
+ * Inject one more unit into an IN-FLIGHT `execute` run — a LIVE counterpart to a
2351
+ * `Controller.spawn`, called from OUTSIDE any unit's handler.
2352
+ *
2353
+ * @remarks
2354
+ * Returns `undefined` synchronously (graceful, non-throwing — AGENTS §12) unless the
2355
+ * runner is currently mid-`execute` and not yet stopped — covering "never started",
2356
+ * "already drained", "aborted", and "destroyed". Otherwise the unit is routed through
2357
+ * the SAME backing queue as a declared/`spawn`ed unit via `#launch` — the outstanding-
2358
+ * unit count gate increments BEFORE this call returns, so an in-flight `execute`
2359
+ * keeps awaiting it (the drain race: `#running` flips to `false` as the very first
2360
+ * step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
2361
+ * method after the run has fully drained is cleanly rejected with `undefined` —
2362
+ * never silently dropped, never hangs `execute`). Emits {@link RunnerEventMap.spawn}
2363
+ * with a `parent` of `undefined` (this call has no spawning unit) once accepted.
2364
+ *
2365
+ * @param input - The unit's work payload
2366
+ * @returns The unit's result promise, or `undefined` when no in-flight run can accept it
2367
+ * @example
2368
+ * ```ts
2369
+ * const runner = createRunner({ handler: (c) => c.input })
2370
+ * const result = runner.execute([1, 2])
2371
+ * const extra = runner.spawn(3) // Promise<number> | undefined
2372
+ * await result
2373
+ * ```
2374
+ */
2375
+ spawn(input) {
2376
+ if (this.#stopped || !this.#running) return void 0;
2377
+ return this.#launch(input, void 0, true);
2378
+ }
1797
2379
  async execute(inputs) {
1798
2380
  if (this.#started) throw new Error("runner has already executed");
1799
2381
  if (this.#stopped) throw new Error("runner is stopped");
@@ -1823,6 +2405,47 @@ var Runner = class {
1823
2405
  this.#stopped = true;
1824
2406
  this.#emitter.emit("abort", reason);
1825
2407
  }
2408
+ /**
2409
+ * Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
2410
+ * `pause`, which holds the NEXT dispatch while any in-flight unit finishes.
2411
+ *
2412
+ * @remarks
2413
+ * A no-op once the runner is `stopped` — a stopped runner has no dispatch left to
2414
+ * suspend, mirroring the guard `stop()` itself applies. Also a no-op when already
2415
+ * `paused` (the queue's own `pause` is idempotent), so calling it repeatedly is safe.
2416
+ */
2417
+ pause() {
2418
+ if (this.#stopped || this.#queue.paused) return;
2419
+ this.#queue.pause();
2420
+ }
2421
+ /**
2422
+ * Continue a paused runner (AGENTS §10); delegates to the backing queue's `resume`.
2423
+ *
2424
+ * @remarks
2425
+ * A no-op once the runner is `stopped` (nothing left to resume) and a no-op when the
2426
+ * runner is not currently `paused`, so calling it repeatedly or on a never-paused
2427
+ * runner is safe.
2428
+ */
2429
+ resume() {
2430
+ if (this.#stopped || !this.#queue.paused) return;
2431
+ this.#queue.resume();
2432
+ }
2433
+ /**
2434
+ * Permanently end the runner (AGENTS §10) — a GRACEFUL stop, distinct from `abort`.
2435
+ * Marks the runner `stopping` + `stopped`, then stops the backing queue: every
2436
+ * still-PENDING (never-dispatched) unit is rejected by the queue with its own
2437
+ * "queue is stopped" error, WITHOUT running its handler; every already-in-flight unit
2438
+ * keeps running to completion and settles normally. `#settle` reads `#stopping` to
2439
+ * classify a never-dispatched unit's rejection as a stop artifact (decrement the count
2440
+ * gate, no recorded failure, no fail-fast trip) rather than a genuine failure — a
2441
+ * dispatched unit's rejection while stopping is still a real failure. Idempotent.
2442
+ */
2443
+ stop() {
2444
+ if (this.#stopped) return;
2445
+ this.#stopping = true;
2446
+ this.#stopped = true;
2447
+ this.#queue.stop();
2448
+ }
1826
2449
  destroy() {
1827
2450
  if (this.#stopped) {
1828
2451
  this.#queue.destroy();
@@ -1831,13 +2454,13 @@ var Runner = class {
1831
2454
  this.abort();
1832
2455
  this.#queue.destroy();
1833
2456
  }
1834
- #launch(input, parent) {
2457
+ #launch(input, parent, announce = parent !== void 0) {
1835
2458
  const id = crypto.randomUUID();
1836
2459
  const abort = createAbort();
1837
2460
  this.#aborts.set(id, abort);
1838
2461
  this.#order.push(id);
1839
2462
  this.#count += 1;
1840
- if (parent !== void 0) this.#emitter.emit("spawn", id, parent);
2463
+ if (announce) this.#emitter.emit("spawn", id, parent);
1841
2464
  const promise = this.#queue.enqueue({
1842
2465
  id,
1843
2466
  input
@@ -1858,6 +2481,7 @@ var Runner = class {
1858
2481
  #dispatch(unit, execution) {
1859
2482
  const abort = this.#aborts.get(unit.id);
1860
2483
  if (abort === void 0) throw new Error("unit abort missing");
2484
+ this.#dispatched.add(unit.id);
1861
2485
  const controller = new Controller(unit.id, unit.input, abort, execution.signal, (input) => this.#spawn(input, unit.id));
1862
2486
  this.#emitter.emit("unit", unit.id);
1863
2487
  return this.#handler(controller);
@@ -1870,7 +2494,7 @@ var Runner = class {
1870
2494
  if (outcome.ok) {
1871
2495
  this.#values.set(id, { value: outcome.value });
1872
2496
  this.#emitter.emit("settle", id);
1873
- } else if (this.#failure === void 0) {
2497
+ } else if (this.#stopping && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
1874
2498
  this.#failure = { error: outcome.error };
1875
2499
  this.#emitter.emit("fail", id, outcome.error);
1876
2500
  this.abort(outcome.error);
@@ -1938,35 +2562,51 @@ var TaskController = class {
1938
2562
  //#region src/core/WorkflowRunner.ts
1939
2563
  /**
1940
2564
  * 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.
2565
+ * substrate — phases sequential, tasks concurrent — dispatching each task through its OWN
2566
+ * resolved handler under the `bail` policy.
1943
2567
  *
1944
2568
  * @remarks
1945
2569
  * - **Composes, never re-implements.** Per-phase bounded concurrency is one
1946
2570
  * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
1947
2571
  * `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} +
2572
+ * timeout / budget / entity `signal` fold through {@link createAbort} / {@link createTimeout} +
1949
2573
  * `AbortSignal.any` (exactly as the agent runtime folds its bounds); pacing is the shipped
1950
2574
  * {@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.
2575
+ * its own — it only sequences phases, dispatches a task's own handler, and drives the live
2576
+ * entity.
2577
+ * - **Pure engine no registries, no tool/agent knowledge.** The runner carries no
2578
+ * `functions` / `tools` / `agents` registry: each live {@link TaskInterface} already
2579
+ * resolved its own {@link import('./types.js').WorkflowFunction} into
2580
+ * {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
2581
+ * or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
2582
+ * dispatch is simply "invoke the task's own handler". Static tool / agent calling is an
2583
+ * OPT-IN concern of `factories.ts`'s adapter factories ({@link import('./factories.js').createToolFunction},
2584
+ * {@link import('./factories.js').createAgentFunction}) plain {@link import('./types.js').WorkflowFunction}s a
2585
+ * caller wires into {@link WorkflowOptions.functions} like any other behavior. This module
2586
+ * never imports `@orkestrel/agent`.
2587
+ * - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
2588
+ * from a {@link WorkflowDefinition} (single source of truth for the `run` / `concurrency`
2589
+ * metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
2590
+ * {@link WorkflowInterface} instead the entity-native control surface (AGENTS §10:
2591
+ * `pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
2592
+ * converge on the SAME `#execute` engine: neither reads a `WorkflowDefinition` once the tree
2593
+ * exists `#runTask` reads each task's OWN {@link import('./types.js').TaskInterface.handler}
2594
+ * / `retries` / `timeout`, and `#runPhase` reads each phase's OWN
2595
+ * {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
2596
+ * runs under EXACTLY the same rules as one built from the original definition.
2597
+ * - **Phases sequential, tasks concurrent — LIVE continuity.** `#execute` drives the phases in
2598
+ * order, RE-READING `workflow.phases.phases()` every iteration (a cursor over the live
2599
+ * manager, not a one-time snapshot) so a caller's `workflow.add(phaseDefinition)` mid-run is
2600
+ * picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event BEFORE
2601
+ * capturing its task list, then `spawn`s any task added mid-phase onto the SAME substrate
2602
+ * Runner (so it is actually dispatched, under the same `concurrency`); a task added too late
2603
+ * for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
2604
+ * phase always reaches a coherent terminal state.
2605
+ * - **Dispatch by handler.** `#runTask` invokes the live task's own
2606
+ * {@link import('./types.js').TaskInterface.handler} directly: `undefined` (an omitted `run`,
2607
+ * or a `run` name absent from the {@link WorkflowOptions.functions} registry it was resolved
2608
+ * against) AUTO-COMPLETES — the ROADMAP no-handler rule; otherwise the handler runs with the
2609
+ * task's {@link import('./types.js').TaskControllerInterface} handle.
1970
2610
  * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
1971
2611
  * THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
1972
2612
  * (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
@@ -1974,7 +2614,20 @@ var TaskController = class {
1974
2614
  * Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
1975
2615
  * the Runner settles every unit (allSettled) and the run finishes (the workflow derives
1976
2616
  * `completed`, the failure recorded in the result tree).
1977
- * - **Abort / Timeout / Budget fold.** `#execute` folds the run's external `signal`, a
2617
+ * - **Pause / stop / destroy gates.** `workflow.pause()` is honoured at exactly two points —
2618
+ * the next phase boundary (workflow-only) and each task's own pre-dispatch (before
2619
+ * `task.start()`, workflow gate FIRST then this task's own `phase.pause()`) — by parking on
2620
+ * {@link WorkflowInterface.wait} / {@link PhaseInterface.wait}; an in-flight task body is
2621
+ * NEVER suspended mid-flight. A GRACEFUL `workflow.stop()` (no signal involved) is caught at
2622
+ * those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
2623
+ * HARD `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
2624
+ * into the run's composed signal — so it cancels the active phase Runner (and every
2625
+ * in-flight task) exactly like an external abort / timeout / budget fire. EVERY park on a
2626
+ * `wait()` gate is RACED against that same run signal (`#raceWait`, S2) — so a cancel firing
2627
+ * WHILE parked unparks the engine promptly instead of hanging until `resume`; the existing
2628
+ * halt / abort re-checks after the gate then decide the outcome.
2629
+ * - **Abort / Timeout / Budget / entity-signal fold.** `#execute` folds the live workflow's
2630
+ * own {@link WorkflowInterface.signal}, the run's external `signal`, a
1978
2631
  * {@link TimeoutInterface}, and the `@orkestrel/budget` package's `BudgetInterface`'s
1979
2632
  * `signal` into one `runSignal` (`AbortSignal.any`); a fire aborts the active phase's Runner
1980
2633
  * (cancelling every in-flight task) and HALTS the run — the remaining tasks / phases `skip`
@@ -1982,59 +2635,69 @@ var TaskController = class {
1982
2635
  * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
1983
2636
  * `runSignal`, so a handler observes either cause directly.
1984
2637
  * - **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.
2638
+ * each `#execute`, so a nested `execute` (a bound workflow-tool handler re-entering this
2639
+ * instance while the outer run is suspended awaiting it) cannot clobber the outer run's state.
1987
2640
  */
1988
2641
  var WorkflowRunner = class {
1989
- #functions;
1990
- #tools;
1991
- #agents;
1992
2642
  #scheduler;
1993
- #workflowTool;
1994
- constructor(functions, tools, agents, scheduler, workflowTool) {
1995
- this.#functions = functions;
1996
- this.#tools = tools;
1997
- this.#agents = agents;
2643
+ constructor(scheduler) {
1998
2644
  this.#scheduler = scheduler;
1999
- this.#workflowTool = workflowTool;
2000
2645
  }
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);
2646
+ execute(target, options) {
2647
+ if (this.#isWorkflow(target)) {
2648
+ if (target.status !== "pending" || target.destroyed) throw new WorkflowError("TRANSITION", `workflow '${target.id}' is not drivable`, {
2649
+ id: target.id,
2650
+ status: target.status,
2651
+ destroyed: target.destroyed
2652
+ });
2653
+ return this.#execute(target, options);
2654
+ }
2655
+ const workflow = new Workflow(definitionToSnapshot(target, options?.bail ?? target.bail ?? false), options);
2656
+ return this.#execute(workflow, options);
2006
2657
  }
2007
- async #execute(workflow, definition, options, depth, ancestry) {
2658
+ async #execute(workflow, options) {
2008
2659
  const ms = options?.timeout;
2009
2660
  const timeout = ms !== void 0 && ms > 0 ? createTimeout({ ms }) : void 0;
2010
2661
  timeout?.start();
2011
2662
  options?.budget?.start();
2012
- const runSignal = this.#fold(options, timeout);
2663
+ const runSignal = this.#fold(workflow, options, timeout);
2013
2664
  const holder = { runner: void 0 };
2014
- const onCancel = () => holder.runner?.abort(runSignal?.reason);
2015
- if (runSignal !== void 0) if (runSignal.aborted) onCancel();
2665
+ const onCancel = () => holder.runner?.abort(runSignal.reason);
2666
+ if (runSignal.aborted) onCancel();
2016
2667
  else runSignal.addEventListener("abort", onCancel, { once: true });
2017
2668
  try {
2018
- const phases = workflow.phases.phases();
2019
- for (let index = 0; index < phases.length; index += 1) {
2669
+ let index = 0;
2670
+ for (;;) {
2671
+ const phases = workflow.phases.phases();
2672
+ if (index >= phases.length) break;
2020
2673
  const phase = phases[index];
2021
- if (phase === void 0) continue;
2674
+ if (phase === void 0) {
2675
+ index += 1;
2676
+ continue;
2677
+ }
2022
2678
  if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2023
- this.#skipFrom(phases, index);
2679
+ this.#haltFrom(phases, index, workflow, runSignal);
2024
2680
  break;
2025
2681
  }
2026
- if (await this.#runPhase(workflow, phase, this.#phaseOf(definition, phase.id), runSignal, holder, depth, ancestry)) {
2027
- this.#skipFrom(phases, index + 1);
2682
+ if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
2683
+ if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2684
+ this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
2685
+ break;
2686
+ }
2687
+ if (await this.#runPhase(workflow, phase, runSignal, holder)) {
2688
+ this.#skipFrom(workflow.phases.phases(), index + 1);
2028
2689
  break;
2029
2690
  }
2030
- if (index < phases.length - 1 && !this.#cancelled(runSignal)) try {
2031
- await this.#scheduler.yield(runSignal === void 0 ? void 0 : { signal: runSignal });
2032
- } catch {}
2691
+ index += 1;
2692
+ const remaining = workflow.phases.phases();
2693
+ if (index < remaining.length && !this.#cancelled(runSignal)) try {
2694
+ await this.#scheduler.yield({ signal: runSignal });
2695
+ } catch (error) {
2696
+ if (!runSignal.aborted) throw error;
2697
+ }
2033
2698
  }
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();
2699
+ if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
2700
+ else if (this.#completable(workflow)) workflow.complete();
2038
2701
  return {
2039
2702
  workflow,
2040
2703
  status: workflow.status,
@@ -2042,52 +2705,71 @@ var WorkflowRunner = class {
2042
2705
  };
2043
2706
  } finally {
2044
2707
  timeout?.clear();
2045
- runSignal?.removeEventListener("abort", onCancel);
2708
+ runSignal.removeEventListener("abort", onCancel);
2046
2709
  }
2047
2710
  }
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;
2711
+ async #runPhase(workflow, phase, runSignal, holder) {
2712
+ const launched = /* @__PURE__ */ new Set();
2713
+ let runner;
2714
+ const onAdd = (task) => {
2715
+ if (launched.has(task.id)) return;
2716
+ launched.add(task.id);
2717
+ runner?.spawn(task);
2718
+ };
2719
+ phase.emitter.on("add", onAdd);
2066
2720
  try {
2067
- await runner.execute(tasks);
2068
- return false;
2069
- } catch {
2070
- return !this.#cancelled(runSignal);
2721
+ const tasks = phase.tasks.tasks();
2722
+ for (const task of tasks) launched.add(task.id);
2723
+ if (tasks.length === 0) return false;
2724
+ const bail = phase.bail;
2725
+ const concurrency = phase.concurrency !== void 0 && phase.concurrency > 0 ? phase.concurrency : DEFAULT_PHASE_CONCURRENCY;
2726
+ const attempts = /* @__PURE__ */ new Map();
2727
+ const created = new Runner({
2728
+ concurrency,
2729
+ entries: (task) => ({
2730
+ retries: task.retries,
2731
+ timeout: task.timeout
2732
+ }),
2733
+ handler: (controller) => this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts)
2734
+ });
2735
+ runner = created;
2736
+ holder.runner = created;
2737
+ try {
2738
+ await created.execute(tasks);
2739
+ return false;
2740
+ } catch {
2741
+ return !this.#cancelled(runSignal);
2742
+ } finally {
2743
+ created.destroy();
2744
+ holder.runner = void 0;
2745
+ }
2071
2746
  } finally {
2072
- runner.destroy();
2073
- holder.runner = void 0;
2747
+ phase.emitter.off("add", onAdd);
2748
+ if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2749
+ for (const task of phase.tasks.tasks()) this.#skip(task);
2074
2750
  }
2075
2751
  }
2076
- async #runTask(workflow, task, definition, controller, runSignal, bail, attempts, depth, ancestry) {
2752
+ async #runTask(workflow, task, controller, runSignal, bail, attempts) {
2077
2753
  const signal = this.#taskSignal(controller.signal, runSignal);
2078
2754
  const attempt = (attempts.get(task.id) ?? 0) + 1;
2079
2755
  attempts.set(task.id, attempt);
2080
- const last = attempt > Math.max(0, definition?.retries ?? 0);
2756
+ const last = attempt > Math.max(0, task.retries ?? 0);
2757
+ if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
2758
+ if (task.phase.paused) await this.#raceWait(() => task.phase.wait(), runSignal);
2759
+ if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2760
+ this.#skipCancelled(task, workflow, runSignal);
2761
+ return;
2762
+ }
2081
2763
  if (task.status === "pending") task.start();
2082
- if (this.#skipping(controller, runSignal)) {
2083
- this.#skip(task);
2764
+ if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2765
+ this.#skipCancelled(task, workflow, runSignal);
2084
2766
  return;
2085
2767
  }
2086
2768
  const handle = new TaskController(signal, task.snapshot().metadata, task.context, () => workflow.results());
2087
2769
  try {
2088
- const value = await this.#dispatch(definition, handle, depth, ancestry);
2770
+ const value = task.handler === void 0 ? void 0 : await task.handler(handle);
2089
2771
  if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2090
- this.#skip(task);
2772
+ this.#skipCancelled(task, workflow, runSignal);
2091
2773
  return;
2092
2774
  }
2093
2775
  if (signal.aborted) {
@@ -2097,7 +2779,7 @@ var WorkflowRunner = class {
2097
2779
  task.complete(value);
2098
2780
  } catch (error) {
2099
2781
  if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2100
- this.#skip(task);
2782
+ this.#skipCancelled(task, workflow, runSignal);
2101
2783
  return;
2102
2784
  }
2103
2785
  if (signal.aborted) {
@@ -2113,80 +2795,32 @@ var WorkflowRunner = class {
2113
2795
  if (!last) return;
2114
2796
  task.fail(/* @__PURE__ */ new Error(`task '${task.id}' timed out`));
2115
2797
  }
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]
2798
+ async #raceWait(wait, runSignal) {
2799
+ if (runSignal.aborted) return;
2800
+ let onAbort;
2801
+ const cancelled = new Promise((resolve) => {
2802
+ onAbort = () => resolve();
2803
+ runSignal.addEventListener("abort", onAbort, { once: true });
2151
2804
  });
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
2805
  try {
2173
- return await agent.generate();
2806
+ await Promise.race([wait(), cancelled]);
2174
2807
  } finally {
2175
- signal.removeEventListener("abort", onAbort);
2808
+ if (onAbort !== void 0) runSignal.removeEventListener("abort", onAbort);
2176
2809
  }
2177
2810
  }
2178
2811
  #taskSignal(unitSignal, runSignal) {
2179
- if (runSignal === void 0) return unitSignal;
2180
- return createAbort({ signal: AbortSignal.any([unitSignal, runSignal]) }).signal;
2812
+ return AbortSignal.any([unitSignal, runSignal]);
2181
2813
  }
2182
- #fold(options, timeout) {
2183
- const signals = [];
2814
+ #fold(workflow, options, timeout) {
2815
+ const signals = [workflow.signal];
2184
2816
  if (options?.signal !== void 0) signals.push(options.signal);
2185
2817
  if (timeout !== void 0) signals.push(timeout.signal);
2186
2818
  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);
2819
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
2820
+ }
2821
+ #haltFrom(phases, index, workflow, runSignal) {
2822
+ if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2823
+ this.#skipFrom(phases, index);
2190
2824
  }
2191
2825
  #skipFrom(phases, index) {
2192
2826
  for (let cursor = index; cursor < phases.length; cursor += 1) {
@@ -2195,14 +2829,18 @@ var WorkflowRunner = class {
2195
2829
  for (const task of phase.tasks.tasks()) this.#skip(task);
2196
2830
  }
2197
2831
  }
2832
+ #skipCancelled(task, workflow, runSignal) {
2833
+ if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2834
+ this.#skip(task);
2835
+ }
2198
2836
  #skip(task) {
2199
2837
  if (task.status === "pending" || task.status === "running") task.skip();
2200
2838
  }
2201
2839
  #skipping(controller, runSignal) {
2202
- return controller.aborted || runSignal?.aborted === true;
2840
+ return controller.aborted || runSignal.aborted;
2203
2841
  }
2204
2842
  #cancelled(runSignal) {
2205
- return runSignal?.aborted === true;
2843
+ return runSignal.aborted;
2206
2844
  }
2207
2845
  #halted(workflow) {
2208
2846
  const status = workflow.status;
@@ -2215,11 +2853,8 @@ var WorkflowRunner = class {
2215
2853
  #completable(workflow) {
2216
2854
  return workflow.status === "pending";
2217
2855
  }
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);
2856
+ #isWorkflow(target) {
2857
+ return "destroyed" in target && "snapshot" in target && typeof target.snapshot === "function";
2223
2858
  }
2224
2859
  };
2225
2860
  //#endregion
@@ -2311,6 +2946,11 @@ function createWorkflowDraftContract() {
2311
2946
  * `options.phases[id].tasks[id]` (the AGENTS §8 nested-by-id bag). The W-b tree is the
2312
2947
  * state machine ONLY — it does not execute tasks (W-c drives the transitions).
2313
2948
  *
2949
+ * `options.functions` is the {@link import('./types.js').WorkflowFunctions} registry each live
2950
+ * task's `run` name resolves against ONCE at construction into its runtime
2951
+ * {@link import('./types.js').TaskInterface.handler} — a name omitted or absent from the
2952
+ * registry resolves to no handler (the no-handler rule).
2953
+ *
2314
2954
  * @param definition - The workflow definition to bring to life
2315
2955
  * @param options - Runtime options (initial listeners, `bail` override, per-node options)
2316
2956
  * @returns The live {@link WorkflowInterface} root
@@ -2370,10 +3010,13 @@ function restoreWorkflow(snapshot, options) {
2370
3010
  * untrusted JSON, so a status (or an override) outside
2371
3011
  * {@link import('./constants.js').WORKFLOW_STATUSES} /
2372
3012
  * {@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),
3013
+ * a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
3014
+ * present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
3015
+ * `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
2374
3016
  * 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.
3017
+ * The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
3018
+ * checked WHEN present. Structural shape beyond these fields is the contract's concern; this
3019
+ * guards exactly the fields the live state machine reads back.
2377
3020
  *
2378
3021
  * @param snapshot - The snapshot to validate
2379
3022
  */
@@ -2403,10 +3046,28 @@ function assertSnapshot(snapshot) {
2403
3046
  phase: phase.id,
2404
3047
  override: phase.override
2405
3048
  });
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
3049
+ if (phase.concurrency !== void 0 && (!Number.isInteger(phase.concurrency) || phase.concurrency < 1)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid concurrency`, {
3050
+ phase: phase.id,
3051
+ concurrency: phase.concurrency
2409
3052
  });
3053
+ for (const task of phase.tasks) {
3054
+ if (!TASK_STATUSES.includes(task.status)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid status`, {
3055
+ task: task.id,
3056
+ status: task.status
3057
+ });
3058
+ if (task.run !== void 0 && task.run.length < 1) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid run`, {
3059
+ task: task.id,
3060
+ run: task.run
3061
+ });
3062
+ if (task.retries !== void 0 && (!Number.isInteger(task.retries) || task.retries < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid retries`, {
3063
+ task: task.id,
3064
+ retries: task.retries
3065
+ });
3066
+ if (task.timeout !== void 0 && (!Number.isInteger(task.timeout) || task.timeout < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid timeout`, {
3067
+ task: task.id,
3068
+ timeout: task.timeout
3069
+ });
3070
+ }
2410
3071
  }
2411
3072
  }
2412
3073
  /**
@@ -2485,78 +3146,194 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
2485
3146
  /**
2486
3147
  * Create a workflow runner — a {@link WorkflowRunnerInterface} that EXECUTES a live W-b
2487
3148
  * workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent,
2488
- * each task dispatched BY NAME under the workflow's `bail` policy.
3149
+ * each task dispatched through its OWN resolved handler under the workflow's `bail` policy.
2489
3150
  *
2490
3151
  * @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
3152
+ * The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
3153
+ * carries no `functions` / `tools` / `agents` registry of its own: each live task already
3154
+ * resolved its own {@link import('./types.js').WorkflowFunction} into
3155
+ * {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
3156
+ * {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
3157
+ * Per-phase bounded concurrency is one {@link createRunner} per phase; `bail` maps onto that
3158
+ * Runner's fail-fast (`true` the first failure aborts the in-flight siblings + skips the
3159
+ * rest) vs settle-all (`false` failures are recorded, the run finishes); the run-level abort
3160
+ * / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
3161
+ * `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped scheduler.
3162
+ * `execute(definition, options?)` BUILDS the live tree from the definition itself (via
3163
+ * {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
3164
+ * the live entity (`start` → `complete` / `fail`), and resolves a
2500
3165
  * {@link import('./types.js').WorkflowResult}.
2501
3166
  *
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}.
3167
+ * Static tool / agent calling is OPT-IN, wired through the adapter factories
3168
+ * {@link createToolFunction} / {@link createAgentFunction} plain
3169
+ * {@link import('./types.js').WorkflowFunction}s a caller composes into its OWN
3170
+ * {@link WorkflowOptions.functions} registry, same as any other behavior. A task with no
3171
+ * resolved handler AUTO-COMPLETES (the ROADMAP no-handler rule).
3172
+ *
3173
+ * @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
3174
+ * See {@link WorkflowRunnerOptions}.
2517
3175
  * @returns A working {@link WorkflowRunnerInterface}
2518
3176
  *
2519
3177
  * @example
2520
3178
  * ```ts
2521
- * import { createWorkflowRunner, createToolManager } from '@src/core'
3179
+ * import { createWorkflowRunner } from '@src/core'
2522
3180
  *
2523
- * const tools = createToolManager()
2524
- * const runner = createWorkflowRunner({
2525
- * functions: { compile: async (controller) => `built ${controller.task.id}` },
2526
- * tools,
2527
- * })
3181
+ * const runner = createWorkflowRunner()
2528
3182
  * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
2529
- * { id: 't', name: 'T', run: { via: 'function', name: 'compile' } },
3183
+ * { id: 't', name: 'T', run: 'compile' },
2530
3184
  * ] }] }
2531
- * const result = await runner.execute(definition) // builds + drives the tree
3185
+ * const result = await runner.execute(definition, {
3186
+ * functions: { compile: async (controller) => `built ${controller.task.id}` },
3187
+ * })
2532
3188
  * result.status // 'completed'
2533
3189
  * result.workflow.phase('p')?.task('t')?.status // 'completed'
2534
3190
  * ```
2535
3191
  */
2536
3192
  function createWorkflowRunner(options) {
2537
- return new WorkflowRunner(options?.functions ?? {}, options?.tools, options?.agents, options?.scheduler ?? createScheduler(), createWorkflowTool);
3193
+ return new WorkflowRunner(options?.scheduler ?? createScheduler());
3194
+ }
3195
+ /**
3196
+ * Wrap a registered tool as a {@link WorkflowFunction} — the OPT-IN adapter that lets a
3197
+ * `function`-form task run a `@orkestrel/agent` tool BY NAME.
3198
+ *
3199
+ * @remarks
3200
+ * Composes into a caller's {@link WorkflowOptions.functions} registry like any other behavior
3201
+ * (`{ publish: createToolFunction(tools, 'publish') }`); the PURE
3202
+ * {@link import('./WorkflowRunner.js').WorkflowRunner} has no knowledge of tools itself. The
3203
+ * returned function executes `name` against `tools` with the task's `controller.input` as the
3204
+ * call arguments, id-correlated to the task's own id. A `ToolManagerInterface.execute` NEVER
3205
+ * throws (a handler throw is isolated into `result.error`), so a failing tool is surfaced here
3206
+ * as a THROWN `Error` carrying the original message as `cause` — the leaf `fail`s, honouring
3207
+ * `bail`. An UNREGISTERED tool name is a programmer error (an explicit binding to a name that
3208
+ * doesn't exist) — unlike the engine's own silent auto-complete of an unresolved task handler,
3209
+ * this THROWS a typed `TOOL` {@link WorkflowError}.
3210
+ *
3211
+ * @param tools - The {@link ToolManagerInterface} the named tool is registered on
3212
+ * @param name - The registered tool's name
3213
+ * @returns A {@link WorkflowFunction} that runs the named tool
3214
+ *
3215
+ * @example
3216
+ * ```ts
3217
+ * import { createToolFunction, createToolManager, createWorkflowRunner } from '@src/core'
3218
+ *
3219
+ * const tools = createToolManager()
3220
+ * tools.add(myPublishTool)
3221
+ * const runner = createWorkflowRunner()
3222
+ * await runner.execute(definition, { functions: { publish: createToolFunction(tools, 'publish') } })
3223
+ * ```
3224
+ */
3225
+ function createToolFunction(tools, name) {
3226
+ return async (controller) => {
3227
+ if (tools.tool(name) === void 0) throw new WorkflowError("TOOL", `tool '${name}' is not registered`, { tool: name });
3228
+ const result = await tools.execute({
3229
+ id: controller.task.id,
3230
+ name,
3231
+ arguments: controller.input
3232
+ });
3233
+ if (result.error !== void 0) throw new Error(result.error, { cause: result.error });
3234
+ return result.value;
3235
+ };
3236
+ }
3237
+ /**
3238
+ * Wrap a live `AgentInterface` (`@orkestrel/agent`) as a {@link WorkflowFunction} — the OPT-IN
3239
+ * adapter that runs the agent to a settled result, folding a nested workflow-authoring
3240
+ * depth / cycle guard into its own closure.
3241
+ *
3242
+ * @remarks
3243
+ * Composes into a caller's {@link WorkflowOptions.functions} registry like any other behavior;
3244
+ * the PURE {@link import('./WorkflowRunner.js').WorkflowRunner} has no knowledge of agents
3245
+ * itself. Before running the agent, the depth/cycle guard REJECTS the call (a THROWN typed
3246
+ * `DEPTH` {@link WorkflowError}, which the leaf `fail`s) when running it would push a nested
3247
+ * chain past {@link MAX_WORKFLOW_DEPTH}, OR when this agent is already an ancestor (a cycle) —
3248
+ * ported from the former engine-side guard. When {@link AgentFunctionOptions.runner} is
3249
+ * supplied, the adapter BINDS a depth/cycle-aware {@link createWorkflowTool} onto the agent's
3250
+ * `context.tools` (the propagation seam) — closed over `depth` and the extended ancestry (the
3251
+ * tool itself computes `depth + 1` internally) — so the agent can author + run a NESTED
3252
+ * workflow through it; the wrapped default is the CURRENT task's own workflow id (used only on
3253
+ * a no-args tool call). The task's cancellation folds into the agent run: an already-aborted
3254
+ * `controller.signal` cancels the agent up front; otherwise a one-shot listener fires
3255
+ * `agent.abort(reason)` when the task cancels, removed in `finally`. `agent.generate()` resolves
3256
+ * a partial `AgentResult` on a cancel (never rejects), returned as the task's completed value.
3257
+ *
3258
+ * A bound agent is effectively SINGLE-RUN: `context.tools.add` binds one {@link ToolInterface}
3259
+ * under the fixed {@link import('./constants.js').WORKFLOW_TOOL_NAME}, and `agent.generate()` /
3260
+ * `agent.abort()` are per-agent state. Two CONCURRENT tasks sharing the SAME `agent` instance
3261
+ * race on that one tool binding (last-write-wins) and on generate/abort — give each concurrent
3262
+ * task its OWN agent instance.
3263
+ *
3264
+ * @param agent - The live `AgentInterface` to run
3265
+ * @param options - The nested-workflow binding + depth/cycle bookkeeping (see {@link AgentFunctionOptions})
3266
+ * @returns A {@link WorkflowFunction} that runs `agent` to its settled result
3267
+ *
3268
+ * @example
3269
+ * ```ts
3270
+ * import { createAgentFunction, createWorkflowRunner } from '@src/core'
3271
+ *
3272
+ * const runner = createWorkflowRunner()
3273
+ * const review = createAgentFunction(myAgent, { runner })
3274
+ * await runner.execute(definition, { functions: { review } })
3275
+ * ```
3276
+ */
3277
+ function createAgentFunction(agent, options) {
3278
+ return async (controller) => {
3279
+ const depth = options?.depth ?? 0;
3280
+ const ancestry = options?.ancestry ?? [];
3281
+ if (depth + 1 > 8) throw new WorkflowError("DEPTH", `agent '${agent.id}' exceeds max workflow depth`, {
3282
+ agent: agent.id,
3283
+ depth,
3284
+ max: 8
3285
+ });
3286
+ const tag = agentTag(agent.id);
3287
+ if (ancestry.includes(tag)) throw new WorkflowError("DEPTH", `agent '${agent.id}' is already an ancestor (cycle)`, {
3288
+ agent: agent.id,
3289
+ ancestry: [...ancestry]
3290
+ });
3291
+ const runner = options?.runner;
3292
+ if (runner !== void 0) {
3293
+ const workflowId = controller.task.phase.workflow.id;
3294
+ const wrapped = {
3295
+ id: workflowId,
3296
+ name: workflowId,
3297
+ phases: []
3298
+ };
3299
+ agent.context.tools.add(createWorkflowTool(wrapped, runner, {
3300
+ depth,
3301
+ ancestry: [...ancestry, tag]
3302
+ }));
3303
+ }
3304
+ const signal = controller.signal;
3305
+ const onAbort = () => agent.abort(signal.reason);
3306
+ if (signal.aborted) agent.abort(signal.reason);
3307
+ else signal.addEventListener("abort", onAbort, { once: true });
3308
+ try {
3309
+ return await agent.generate();
3310
+ } finally {
3311
+ signal.removeEventListener("abort", onAbort);
3312
+ }
3313
+ };
2538
3314
  }
2539
3315
  /**
2540
3316
  * 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
3317
+ * the SIMPLE flat authoring shape (`{ name?, steps: [{ name }] }`) as its `parameters` so
2542
3318
  * even a small model can author a complete tree, and its handler EXPANDS / COMPLETES the
2543
3319
  * authored blob, validates it against the STRICT contract, runs it through `runner`, and
2544
3320
  * returns the run SUMMARY (throwing a typed {@link WorkflowError} on failure).
2545
3321
  *
2546
3322
  * @remarks
2547
3323
  * 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.
3324
+ * expose it for free — nothing MCP is wired here). It is ALSO the propagation carrier
3325
+ * {@link createAgentFunction} binds onto a wrapped agent's `context.tools`: because a tool
3326
+ * handler receives ONLY the model-supplied `args` (no ambient context, no signal), the run's
3327
+ * depth + ancestry are CLOSED OVER at bind time via {@link WorkflowToolOptions}, and the
3328
+ * handler enforces the SAME depth / cycle guard itself (this function owns it now — the engine
3329
+ * carries none) before running the nested workflow at `depth + 1` with the extended ancestry.
2553
3330
  *
2554
3331
  * **Widened authoring surface (additive — the canonical contract + runner stay STRICT and
2555
3332
  * 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
3333
+ * nested {@link WorkflowDefinition} (six required `id`/`name` strings, an all-or-nothing tree).
3334
+ * So the tool ACCEPTS three authoring forms and converges them on the SAME strict
3335
+ * {@link createWorkflowContract} gate before running (soundness preserved):
3336
+ * - the FLAT shape `{ name?, steps: [{ name }] }` — the ADVERTISED `parameters` (the simplest
2560
3337
  * form, {@link import('./helpers.js').expandSteps}'d into one one-task phase per step);
2561
3338
  * - a nested DRAFT with any `id`/`name` OMITTED — {@link createWorkflowDraftContract}-parsed then
2562
3339
  * {@link import('./helpers.js').completeDraft}'d (missing ids synthesized positionally);
@@ -2578,9 +3355,15 @@ function createWorkflowRunner(options) {
2578
3355
  * gate (e.g. an explicit empty `id`, `concurrency: 0`) ⇒ THROW a `TOOL` {@link WorkflowError} (no run).
2579
3356
  * - **Over-deep / cyclic** ⇒ THROW a `DEPTH` {@link WorkflowError} when the nested run would exceed
2580
3357
  * {@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}).
3358
+ * SAME `code` {@link createAgentFunction}'s own guard raises. Enforced HERE, INSIDE this
3359
+ * handler, before ever calling `runner.execute` the engine itself performs no such check.
3360
+ * - **Otherwise** `runner.execute(target)`, RETURNING the plain summary of the terminal run
3361
+ * (`{ status, count }`, via {@link workflowToolSummary}).
3362
+ *
3363
+ * The tool executes AUTHORED STRUCTURE, not consumer behavior: a nested tree authored through
3364
+ * it (flat, draft, or full form) carries no {@link WorkflowFunctions} registry, so EVERY one of
3365
+ * its tasks auto-completes under the no-handler rule. This handler validates and synthesizes
3366
+ * shape — it never runs a caller's handlers.
2584
3367
  *
2585
3368
  * @param definition - The workflow the tool runs when called with no authored args
2586
3369
  * @param runner - The {@link WorkflowRunnerInterface} that executes the (nested) workflow
@@ -2630,10 +3413,7 @@ function createWorkflowTool(definition, runner, options) {
2630
3413
  workflow: target.id,
2631
3414
  ancestry: [...ancestry]
2632
3415
  });
2633
- return workflowToolSummary(await runner.execute(target, {
2634
- depth: depth + 1,
2635
- ancestry: [...ancestry, tag]
2636
- }));
3416
+ return workflowToolSummary(await runner.execute(target));
2637
3417
  }
2638
3418
  });
2639
3419
  }
@@ -2729,6 +3509,6 @@ function createRunner(options) {
2729
3509
  return new Runner(options);
2730
3510
  }
2731
3511
  //#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 };
3512
+ export { Controller, DEFAULT_BAIL, DEFAULT_PHASE_CONCURRENCY, DatabaseWorkflowStore, MAX_WORKFLOW_DEPTH, MemoryWorkflowStore, PHASE_STATUSES, Phase, PhaseManager, Runner, Scheduler, TASK_STATUSES, TASK_TRANSITIONS, 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, createAgentFunction, createDatabaseWorkflowStore, createDeferred, createMemoryWorkflowStore, createRunner, createScheduler, createToolFunction, createWorkflow, createWorkflowContract, createWorkflowDraftContract, createWorkflowRunner, createWorkflowTool, definitionToSnapshot, deriveBoundary, derivePhaseStatus, deriveWorkflowStatus, expandSteps, failure, findFailure, insertEntry, isTerminalStatus, isWorkflowError, isWorkflowSnapshot, moveEntry, parkSignal, phaseDefinitionToSnapshot, phaseDraftShape, phaseShape, phaseUpdateShape, restoreWorkflow, stepShape, success, taskDefinitionToSnapshot, taskDraftShape, taskShape, taskUpdateShape, workflowDraftShape, workflowShape, workflowStepsShape, workflowTag, workflowToolSummary };
2733
3513
 
2734
3514
  //# sourceMappingURL=index.js.map