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