@orkestrel/workflow 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let _orkestrel_agent = require("@orkestrel/agent");
3
2
  let _orkestrel_contract = require("@orkestrel/contract");
4
3
  let _orkestrel_database = require("@orkestrel/database");
5
- let _orkestrel_emitter = require("@orkestrel/emitter");
6
4
  let _orkestrel_abort = require("@orkestrel/abort");
5
+ let _orkestrel_emitter = require("@orkestrel/emitter");
7
6
  let _orkestrel_timeout = require("@orkestrel/timeout");
8
7
  let _orkestrel_queue = require("@orkestrel/queue");
9
8
  //#region src/core/Scheduler.ts
@@ -83,18 +82,6 @@ var Scheduler = class {
83
82
  /** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
84
83
  var DEFAULT_BAIL = false;
85
84
  /**
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
85
  * Every {@link TaskStatus} value, frozen — the lifecycle vocabulary of a task.
99
86
  *
100
87
  * @remarks
@@ -174,118 +161,23 @@ var TASK_TRANSITIONS = Object.freeze({
174
161
  /**
175
162
  * The default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
176
163
  * 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.
164
+ * throttle — a cap that is effectively unbounded for any realistic phase.
178
165
  *
179
166
  * @remarks
180
167
  * The determinism principle fixes that a phase's tasks run CONCURRENTLY; `concurrency` is
181
168
  * 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".
186
- */
187
- var DEFAULT_PHASE_CONCURRENCY = 1e6;
188
- /**
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.
191
- *
192
- * @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.
199
- */
200
- var MAX_WORKFLOW_DEPTH = 8;
201
- /**
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).
205
- *
206
- * @remarks
207
- * The propagation seam's well-known key: before running an `agent` task, the runner adds a
208
- * {@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.
212
- */
213
- var WORKFLOW_TOOL_NAME = "workflow";
214
- /**
215
- * 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 }] }`.
217
- *
218
- * @remarks
219
- * 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
221
- * ({@link import('./helpers.js').expandSteps}) into a valid {@link WorkflowDefinition}. It
222
- * is embedded VERBATIM in {@link WORKFLOW_TOOL_DESCRIPTION} and guarded by a parity test
223
- * (it must expand to a tree the STRICT contract accepts), so the doc example can never drift.
224
- */
225
- var WORKFLOW_TOOL_FLAT_EXAMPLE = Object.freeze({
226
- name: "release",
227
- steps: Object.freeze([Object.freeze({
228
- name: "compile",
229
- via: "function"
230
- }), Object.freeze({
231
- name: "publish",
232
- via: "tool"
233
- })])
234
- });
235
- /**
236
- * A minimal NESTED authoring example — the ADVANCED escape-hatch form a model may use
237
- * instead of the flat shape: a full {@link WorkflowDefinition}.
238
- *
239
- * @remarks
240
- * The full four-level form, documented in {@link WORKFLOW_TOOL_DESCRIPTION} as the advanced
241
- * alternative. It is embedded VERBATIM and guarded by a parity test (`createWorkflowContract().is`
242
- * must accept it), so the doc example can never drift from a valid definition.
243
- */
244
- var WORKFLOW_TOOL_NESTED_EXAMPLE = Object.freeze({
245
- id: "release",
246
- name: "Release",
247
- phases: Object.freeze([Object.freeze({
248
- id: "build",
249
- name: "Build",
250
- tasks: Object.freeze([Object.freeze({
251
- id: "compile",
252
- name: "Compile",
253
- run: Object.freeze({
254
- via: "function",
255
- name: "compile"
256
- })
257
- })])
258
- })])
259
- });
260
- /**
261
- * The DESCRIPTION {@link import('./factories.js').createWorkflowTool} advertises — a
262
- * multi-line guide that teaches a small model how to author a complete workflow tree.
263
- *
264
- * @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
268
- * {@link WorkflowDefinition} as the ADVANCED form with a minimal example
269
- * ({@link WORKFLOW_TOOL_NESTED_EXAMPLE}). Both examples are interpolated VERBATIM from the
270
- * validated constants, so a parity test pins them — the description can never drift from a
271
- * 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).
273
- */
274
- var WORKFLOW_TOOL_DESCRIPTION = [
275
- "Author and run a workflow (phases run sequentially, the tasks within a phase run concurrently) in one call.",
276
- "",
277
- "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\" }, ... ] }",
279
- "- 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
- "- the top-level \"name\" (the workflow name) is optional. Ids are filled in for you.",
282
- "Example:",
283
- JSON.stringify(WORKFLOW_TOOL_FLAT_EXAMPLE),
284
- "",
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\" }:",
286
- JSON.stringify(WORKFLOW_TOOL_NESTED_EXAMPLE),
287
- "In the nested form you may omit any \"id\"/\"name\" and they are filled in positionally; a provided one is kept."
288
- ].join("\n");
169
+ * all of a phase's tasks at once — modelled as this finite cap so the value flows straight
170
+ * into the substrate {@link import('./types.js').RunnerInterface}'s `concurrency` (which
171
+ * expects a positive integer) without a special unbounded branch. No realistic phase
172
+ * declares enough tasks to reach it, so it behaves as "run them all".
173
+ *
174
+ * WHY `1024` and not a huge sentinel like `1_000_000`: the backing `@orkestrel/queue` Runner
175
+ * EAGERLY spawns one parked worker loop per concurrency unit AT CONSTRUCTION, so this default
176
+ * must be a value whose eager allocation cost is negligible for every default-concurrency
177
+ * phase a million-unit default meant ~1e6 promise/closure allocations per such phase. A
178
+ * phase may still DECLARE a larger explicit `concurrency` and pays that allocation knowingly.
179
+ */
180
+ var DEFAULT_PHASE_CONCURRENCY = 1024;
289
181
  //#endregion
290
182
  //#region src/core/errors.ts
291
183
  /**
@@ -296,11 +188,11 @@ var WORKFLOW_TOOL_DESCRIPTION = [
296
188
  * offending node id / status. Thrown for an illegal lifecycle transition
297
189
  * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
298
190
  * passed to {@link import('./factories.js').restoreWorkflow} (`RESTORE`), an over-deep /
299
- * cyclic nested-workflow dispatch (`DEPTH`), and a malformed
300
- * {@link import('./factories.js').createWorkflowTool} args blob (`TOOL`). On the
301
- * workflow-tool seam the `DEPTH` / `TOOL` throw is ISOLATED by the
302
- * `@orkestrel/agent` package's `ToolManager` into the tool result's
303
- * top-level `error` (AGENTS §14 — the universal tool-handler contract).
191
+ * cyclic nested-workflow dispatch (`DEPTH`), and a malformed workflow-authoring-tool args
192
+ * blob (`TOOL`). `DEPTH` and `TOOL` are public type surface constructed by the
193
+ * `@orkestrel/tool` package's workflow-tool / agent-function adapters; on that seam the
194
+ * throw is ISOLATED by its `ToolManager` into the tool result's top-level `error`
195
+ * (AGENTS §14 — the universal tool-handler contract).
304
196
  */
305
197
  var WorkflowError = class extends Error {
306
198
  code;
@@ -333,66 +225,6 @@ function isWorkflowError(value) {
333
225
  //#endregion
334
226
  //#region src/core/helpers.ts
335
227
  /**
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
- * The ancestry identifier of a workflow run — `workflow:<id>`.
366
- *
367
- * @remarks
368
- * The {@link import('./WorkflowRunner.js').WorkflowRunner}'s cycle guard records one of
369
- * these per workflow in the current nested run chain (carried on
370
- * {@link import('./types.js').WorkflowRunOptions.ancestry}). Tagging the bare id keeps a
371
- * workflow id and an {@link agentTag} agent name in ONE namespaced set without collision,
372
- * so re-entering a workflow OR an agent already in the chain is a single `includes` check.
373
- *
374
- * @param id - The workflow definition's `id`
375
- * @returns The namespaced ancestry tag (`workflow:<id>`)
376
- */
377
- function workflowTag(id) {
378
- return `workflow:${id}`;
379
- }
380
- /**
381
- * The ancestry identifier of an agent in a run chain — `agent:<name>`.
382
- *
383
- * @remarks
384
- * The agent counterpart of {@link workflowTag}: the runner adds one when it dispatches an
385
- * `agent` task, and rejects the task (a typed `DEPTH` `task.fail`) when the same tag is
386
- * already in the ancestry (a re-entry cycle). The `agent:` namespace keeps it distinct
387
- * from a same-string workflow id.
388
- *
389
- * @param name - The agent's registry name (the `agent`-form's `name`)
390
- * @returns The namespaced ancestry tag (`agent:<name>`)
391
- */
392
- function agentTag(name) {
393
- return `agent:${name}`;
394
- }
395
- /**
396
228
  * Test whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
397
229
  * transition further.
398
230
  *
@@ -478,6 +310,36 @@ function deriveWorkflowStatus(phases) {
478
310
  return "skipped";
479
311
  }
480
312
  /**
313
+ * Derive the PENDING SUFFIX boundary of a positional list of {@link LifecycleStatus}es —
314
+ * the index of the first entry in the contiguous trailing run of `pending` entries.
315
+ *
316
+ * @remarks
317
+ * The native, hook-free replacement for a runner-installed cursor (AGENTS §12): a
318
+ * {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
319
+ * reads this over its live phases' statuses to decide which positions are safe to edit.
320
+ * Because entries run SEQUENTIALLY (phases sequential, AGENTS determinism), every
321
+ * already-started entry forms a contiguous LEADING prefix and every still-`pending`
322
+ * entry forms the trailing suffix — so the boundary is simply the count of leading
323
+ * non-`pending` entries: the index of the first `pending` entry, or the full length when
324
+ * none is `pending` (nothing is safely editable). A `pending` container's entries are ALL
325
+ * `pending`, so the boundary is `0` and every position is naturally accepted — callers
326
+ * need no special case for that.
327
+ *
328
+ * @param statuses - The positional list of statuses to derive the boundary from
329
+ * @returns The index of the first `pending` entry, or `statuses.length` when none is `pending`
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * deriveBoundary(['completed', 'running', 'pending', 'pending']) // 2
334
+ * deriveBoundary(['pending', 'pending']) // 0
335
+ * deriveBoundary(['completed', 'completed']) // 2 (nothing pending)
336
+ * ```
337
+ */
338
+ function deriveBoundary(statuses) {
339
+ const index = statuses.findIndex((status) => status === "pending");
340
+ return index === -1 ? statuses.length : index;
341
+ }
342
+ /**
481
343
  * Test whether the live W-b task state machine may move directly from one
482
344
  * {@link TaskStatus} to another — the legal-transition guard.
483
345
  *
@@ -495,6 +357,66 @@ function canTransitionTask(from, to) {
495
357
  return TASK_TRANSITIONS[from].includes(to);
496
358
  }
497
359
  /**
360
+ * Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
361
+ *
362
+ * @typeParam T - The boxed value's type
363
+ * @param value - The value to box
364
+ * @returns A {@link Success} wrapping `value`
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * const result = success(task) // { success: true, value: task }
369
+ * ```
370
+ */
371
+ function success(value) {
372
+ return {
373
+ success: true,
374
+ value
375
+ };
376
+ }
377
+ /**
378
+ * Box an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
379
+ *
380
+ * @typeParam E - The boxed error's type
381
+ * @param error - The error to box
382
+ * @returns A {@link Failure} wrapping `error`
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * const result = failure(new WorkflowError('MUTATION', 'refused')) // { success: false, error }
387
+ * ```
388
+ */
389
+ function failure(error) {
390
+ return {
391
+ success: false,
392
+ error
393
+ };
394
+ }
395
+ /**
396
+ * Find the first {@link TaskResult} in a positional list whose boxed outcome is a
397
+ * `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
398
+ * `fail`-event lookup.
399
+ *
400
+ * @remarks
401
+ * The shared leaf behind {@link import('./phases/Phase.js').Phase} and
402
+ * {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers ITS tier's
403
+ * results (a phase's own settled tasks, a workflow's flattened `results()`) and feeds
404
+ * them here; the tier-local method keeps the §12 invariant throw (a derived `failed`
405
+ * status guarantees a failing result exists) since throwing on `undefined` is
406
+ * orchestration, not a leaf concern.
407
+ *
408
+ * @param results - The results to scan, in any order
409
+ * @returns The first result whose `result.success` is `false`, or `undefined` if none
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * findFailure([completedResult, failedResult]) // failedResult
414
+ * ```
415
+ */
416
+ function findFailure(results) {
417
+ return results.find((result) => result.result?.success === false);
418
+ }
419
+ /**
498
420
  * Build a {@link WorkflowContext} — the identity every level inherits — from a node's
499
421
  * `id` / `name` / optional `description`.
500
422
  *
@@ -570,10 +492,12 @@ function isWorkflowSnapshot(value) {
570
492
  *
571
493
  * @remarks
572
494
  * 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
495
+ * carry over verbatim, as does each phase's `concurrency` (persisted on the
496
+ * {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `run` /
497
+ * `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
498
+ * so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
499
+ * real work). The `bail` policy carries over — at the
500
+ * workflow tier AND, per phase, the
577
501
  * EFFECTIVE policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
578
502
  * snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped now.
579
503
  * {@link import('./factories.js').createWorkflow} builds from this.
@@ -611,6 +535,7 @@ function definitionToSnapshot(definition, bail) {
611
535
  * The snapshot persists the EFFECTIVE failure policy this phase runs under: the phase's own
612
536
  * `bail` when it declares one, else the `workflowBail` it inherits — so a restore reinstates
613
537
  * the same per-phase policy without a silent default (`effectiveBail = phase.bail ?? workflowBail`).
538
+ * `concurrency` (the resource throttle) carries over verbatim, omitted when undefined.
614
539
  *
615
540
  * @param phase - The phase definition to seed from
616
541
  * @param workflowBail - The workflow-level `bail` default the phase inherits when it declares none
@@ -623,6 +548,7 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
623
548
  ...phase.description === void 0 ? {} : { description: phase.description },
624
549
  status: "pending",
625
550
  bail: phase.bail ?? workflowBail,
551
+ ...phase.concurrency === void 0 ? {} : { concurrency: phase.concurrency },
626
552
  tasks: phase.tasks.map((task) => taskDefinitionToSnapshot(task))
627
553
  };
628
554
  }
@@ -631,6 +557,12 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
631
557
  * {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
632
558
  * result yet, empty metadata).
633
559
  *
560
+ * @remarks
561
+ * `run` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
562
+ * phase's `bail` / `concurrency`) — a restore reinstates the same behavior reference and
563
+ * reliability overrides once paired with a {@link import('./types.js').WorkflowOptions.functions}
564
+ * registry.
565
+ *
634
566
  * @param task - The task definition to seed from
635
567
  * @returns An initial {@link TaskSnapshot}
636
568
  */
@@ -640,7 +572,10 @@ function taskDefinitionToSnapshot(task) {
640
572
  name: task.name,
641
573
  ...task.description === void 0 ? {} : { description: task.description },
642
574
  status: "pending",
643
- metadata: {}
575
+ metadata: {},
576
+ ...task.run === void 0 ? {} : { run: task.run },
577
+ ...task.retries === void 0 ? {} : { retries: task.retries },
578
+ ...task.timeout === void 0 ? {} : { timeout: task.timeout }
644
579
  };
645
580
  }
646
581
  /**
@@ -659,131 +594,64 @@ function collectResults(phases) {
659
594
  return phases.flat();
660
595
  }
661
596
  /**
662
- * Summarize a terminal {@link WorkflowResult} into the PLAIN value a
663
- * {@link import('./factories.js').createWorkflowTool} handler returns on success.
664
- *
665
- * @remarks
666
- * This is the run summary the handler returns DIRECTLY — NOT a `ToolResult` (the future `@orkestrel/agent` package).
667
- * The handler conforms to the universal tool-handler contract (AGENTS §14): it returns the plain value
668
- * (and throws on failure), so the `@orkestrel/agent` package's `ToolManager` performs
669
- * the ONE canonical wrap (`{ id, name, value }`) and the model reads exactly this summary — once,
670
- * identically — over BOTH the agent loop and MCP. The summary is LEAN: the workflow's terminal `status`
671
- * and the COUNT of settled task results — enough for a caller / model to react without serializing the
672
- * whole live tree. (It carries no synthetic `id` / `name`: a tool handler has no call id; the manager
673
- * supplies the canonical envelope's identity.)
674
- *
675
- * @param result - The terminal {@link WorkflowResult} the run produced
676
- * @returns The plain success summary — `{ status, count }`
677
- */
678
- function workflowToolSummary(result) {
679
- return {
680
- status: result.status,
681
- count: result.results.length
682
- };
683
- }
684
- /**
685
- * Complete a {@link WorkflowDraft} into a strict {@link WorkflowDefinition} — synthesize
686
- * any MISSING `id` deterministically + positionally, and default any MISSING `name` to
687
- * its (now-resolved) `id`.
597
+ * Insert one `[key, value]` entry at a positional index into a readonly entries array —
598
+ * the pure splice-in step behind an insertion-ordered registry's `add`.
688
599
  *
689
600
  * @remarks
690
- * The positional id scheme is stable and human-legible: the workflow is `wf`, phase `i`
691
- * is `phase-<i>`, and task `j` of that phase is `<phaseId>-task-<j>` (so a provided phase
692
- * id flows into its tasks' synthesized ids). A PROVIDED `id` / `name` at any level is kept
693
- * VERBATIM synthesis touches only the omitted ones. A missing `name` defaults to the
694
- * resolved `id` (never the other way round), so the result always has both. `run`,
695
- * `description`, the per-phase `concurrency` / `bail`, the per-task `retries` / `timeout`, and
696
- * the workflow `bail` carry over unchanged. The result is a complete
697
- * {@link WorkflowDefinition}; the caller still validates it against the STRICT contract.
698
- *
699
- * @param draft - The draft workflow (id/name optional at all three levels)
700
- * @returns A complete {@link WorkflowDefinition} with every id/name filled
701
- */
702
- function completeDraft(draft) {
703
- const id = draft.id ?? "wf";
704
- return {
705
- id,
706
- name: draft.name ?? id,
707
- ...draft.description === void 0 ? {} : { description: draft.description },
708
- phases: draft.phases.map((phase, index) => completePhaseDraft(phase, index)),
709
- ...draft.bail === void 0 ? {} : { bail: draft.bail }
710
- };
711
- }
712
- /**
713
- * Complete one {@link PhaseDraft} into a strict {@link PhaseDefinition} — the per-phase
714
- * step of {@link completeDraft} (phase `index` → `phase-<index>` when its id is omitted).
601
+ * Shared by {@link import('./tasks/TaskManager.js').TaskManager} and
602
+ * {@link import('./phases/PhaseManager.js').PhaseManager}: both convert their
603
+ * insertion-ordered `Map` to `[...map.entries()]`, call this to splice the new entry
604
+ * in at the target index, then rebuild the `Map` from the result (a stateful step that
605
+ * stays a `#` private method this helper does no `Map` construction). Does not
606
+ * mutate `entries`; returns a new array.
607
+ *
608
+ * @typeParam T - The entry's value type
609
+ * @param entries - The current positional entries, in order
610
+ * @param index - The index to insert at (`0` prepends, `entries.length` appends)
611
+ * @param key - The new entry's key
612
+ * @param value - The new entry's value
613
+ * @returns A new entries array with `[key, value]` inserted at `index`
715
614
  *
716
- * @param phase - The draft phase
717
- * @param index - The phase's positional index in the workflow
718
- * @returns A complete {@link PhaseDefinition}
719
- */
720
- function completePhaseDraft(phase, index) {
721
- const id = phase.id ?? `phase-${index}`;
722
- return {
723
- id,
724
- name: phase.name ?? id,
725
- ...phase.description === void 0 ? {} : { description: phase.description },
726
- tasks: phase.tasks.map((task, taskIndex) => completeTaskDraft(task, id, taskIndex)),
727
- ...phase.concurrency === void 0 ? {} : { concurrency: phase.concurrency },
728
- ...phase.bail === void 0 ? {} : { bail: phase.bail }
729
- };
730
- }
731
- /**
732
- * Complete one {@link TaskDraft} into a strict {@link TaskDefinition} — the per-task leaf
733
- * step of {@link completeDraft} (task `index` of phase `<phaseId>` → `<phaseId>-task-<index>`
734
- * when its id is omitted).
735
- *
736
- * @param task - The draft task
737
- * @param phaseId - The (resolved) parent phase id, so the synthesized task id nests under it
738
- * @param index - The task's positional index within its phase
739
- * @returns A complete {@link TaskDefinition}
615
+ * @example
616
+ * ```ts
617
+ * insertEntry([['a', 1], ['b', 2]], 1, 'c', 3) // [['a', 1], ['c', 3], ['b', 2]]
618
+ * ```
740
619
  */
741
- function completeTaskDraft(task, phaseId, index) {
742
- const id = task.id ?? `${phaseId}-task-${index}`;
743
- return {
744
- id,
745
- name: task.name ?? id,
746
- ...task.description === void 0 ? {} : { description: task.description },
747
- run: task.run,
748
- ...task.retries === void 0 ? {} : { retries: task.retries },
749
- ...task.timeout === void 0 ? {} : { timeout: task.timeout }
750
- };
620
+ function insertEntry(entries, index, key, value) {
621
+ const next = [...entries];
622
+ next.splice(index, 0, [key, value]);
623
+ return next;
751
624
  }
752
625
  /**
753
- * Expand a flat {@link WorkflowSteps} blob into a strict {@link WorkflowDefinition} each
754
- * step becomes a one-task phase, IN ORDER.
626
+ * Reposition the entry keyed `key` to a new positional index in a readonly entries
627
+ * array the pure remove-then-reinsert step behind an insertion-ordered registry's
628
+ * `move`.
755
629
  *
756
630
  * @remarks
757
- * The expansion of the tool's ADVERTISED surface (AGENTS §21 the simplest form a small
758
- * 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? }] }`)
767
- * @returns A complete {@link WorkflowDefinition} (one one-task phase per step)
768
- */
769
- function expandSteps(flat) {
770
- return completeDraft({
771
- ...flat.name === void 0 ? {} : { name: flat.name },
772
- phases: flat.steps.map((step) => ({ tasks: [{ run: stepToForm(step) }] }))
773
- });
774
- }
775
- /**
776
- * Convert one flat {@link WorkflowStep} into a {@link TaskForm} — `name` → the form's `name`,
777
- * `via` → the form's discriminant (defaulting to `'function'`).
631
+ * The move counterpart of {@link insertEntry}: finds the entry by `key`, splices it
632
+ * out, then splices it back in at `index`. An absent `key` is a no-op (returns a copy
633
+ * of `entries` unchanged) the caller (`TaskManager.move` / `PhaseManager.move`)
634
+ * already gates on the target's existence before calling this, so the no-op branch is
635
+ * defensive, never reached in practice. Does not mutate `entries`; returns a new array.
636
+ *
637
+ * @typeParam T - The entry's value type
638
+ * @param entries - The current positional entries, in order
639
+ * @param key - The key of the entry to reposition
640
+ * @param index - The new index for the entry
641
+ * @returns A new entries array with the `key` entry repositioned to `index`
778
642
  *
779
- * @param step - The flat step
780
- * @returns The {@link TaskForm} the step's task runs
643
+ * @example
644
+ * ```ts
645
+ * moveEntry([['a', 1], ['b', 2], ['c', 3]], 'a', 2) // [['b', 2], ['c', 3], ['a', 1]]
646
+ * ```
781
647
  */
782
- function stepToForm(step) {
783
- return {
784
- via: step.via ?? "function",
785
- name: step.name
786
- };
648
+ function moveEntry(entries, key, index) {
649
+ const next = [...entries];
650
+ const at = next.findIndex(([entryKey]) => entryKey === key);
651
+ if (at === -1) return next;
652
+ const [entry] = next.splice(at, 1);
653
+ if (entry !== void 0) next.splice(index, 0, entry);
654
+ return next;
787
655
  }
788
656
  /**
789
657
  * Create a {@link DeferredInterface} — a promise whose settlement is driven
@@ -804,41 +672,39 @@ function createDeferred() {
804
672
  reject
805
673
  };
806
674
  }
807
- //#endregion
808
- //#region src/core/shapers.ts
809
675
  /**
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).
676
+ * Park until `signal` aborts — a promise-parked wait (AGENTS §21), never a timer or
677
+ * busy-loop, that NEVER rejects.
814
678
  *
815
679
  * @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`.
680
+ * Resolves IMMEDIATELY when `signal` is already aborted; otherwise attaches a one-shot
681
+ * `abort` listener and resolves when it fires, removing the listener either way. The
682
+ * shared leaf behind the duplicate abort-wiring an execution engine otherwise hand-rolls
683
+ * at every fold point.
684
+ *
685
+ * @param signal - The signal to park on
686
+ * @returns A promise that resolves once `signal` has aborted
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * const controller = new AbortController()
691
+ * const parked = parkSignal(controller.signal)
692
+ * controller.abort()
693
+ * await parked // resolves
694
+ * ```
819
695
  */
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
- }));
696
+ function parkSignal(signal) {
697
+ if (signal.aborted) return Promise.resolve();
698
+ return new Promise((resolve) => {
699
+ signal.addEventListener("abort", () => resolve(), { once: true });
700
+ });
701
+ }
702
+ //#endregion
703
+ //#region src/core/shapers.ts
839
704
  /**
840
- * The shape of a {@link import('./types.js').TaskDefinition} — identity plus the
841
- * behavior reference ({@link taskFormShape}). `description` is optional prose.
705
+ * The shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
706
+ * `run` behavior reference (a plain registry-key string, min length 1). `description` is
707
+ * optional prose.
842
708
  */
843
709
  var taskShape = (0, _orkestrel_contract.objectShape)({
844
710
  id: (0, _orkestrel_contract.stringShape)({
@@ -850,7 +716,10 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
850
716
  description: "Human-readable task name."
851
717
  }),
852
718
  description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Optional task description." })),
853
- run: taskFormShape,
719
+ run: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
720
+ min: 1,
721
+ description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
722
+ })),
854
723
  retries: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
855
724
  min: 0,
856
725
  description: "Extra attempts after the first on failure; overrides the phase default. Omitted means none."
@@ -902,121 +771,41 @@ var workflowShape = (0, _orkestrel_contract.objectShape)({
902
771
  bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
903
772
  });
904
773
  /**
905
- * The shape of a TASK in a draft workflow — identical to {@link taskShape} EXCEPT `id`
906
- * and `name` are OPTIONAL (the tool synthesizes any missing one positionally).
774
+ * The shape of a {@link import('./types.js').TaskUpdate} a partial edit to a
775
+ * `pending` task's `name` / `description`, both optional.
907
776
  *
908
777
  * @remarks
909
- * A PROVIDED `id` / `name` still carries `minLength: 1`, so an explicitly-empty `id: ''`
910
- * is INVALID (rejected by the draft contract), never auto-filled keeping "garbage"
911
- * distinct from "omitted". `run` stays required.
778
+ * Mirrors {@link taskShape}'s `name` / `description` constraints exactly (a provided
779
+ * `name` still has `minLength: 1`); never `id` / `run` / `retries` / `timeout` (those
780
+ * are not patchable fields, AGENTS §12).
912
781
  */
913
- var taskDraftShape = (0, _orkestrel_contract.objectShape)({
914
- id: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
915
- min: 1,
916
- description: "Task id; auto-filled when omitted."
917
- })),
782
+ var taskUpdateShape = (0, _orkestrel_contract.objectShape)({
918
783
  name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
919
784
  min: 1,
920
- description: "Task name; defaults to the id when omitted."
921
- })),
922
- description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Optional task description." })),
923
- run: taskFormShape,
924
- retries: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
925
- min: 0,
926
- description: "Extra attempts after the first on failure; overrides the phase default. Omitted means none."
785
+ description: "New task name."
927
786
  })),
928
- timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
929
- min: 0,
930
- description: "Per-attempt deadline in milliseconds; overrides the phase default. Omitted means no deadline."
931
- }))
787
+ description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "New task description." }))
932
788
  });
933
789
  /**
934
- * The shape of a PHASE in a draft workflow — identical to {@link phaseShape} EXCEPT
935
- * `id` and `name` are OPTIONAL, and its tasks are {@link taskDraftShape}s.
936
- */
937
- var phaseDraftShape = (0, _orkestrel_contract.objectShape)({
938
- id: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
939
- min: 1,
940
- description: "Phase id; auto-filled when omitted."
941
- })),
942
- name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
943
- min: 1,
944
- description: "Phase name; defaults to the id when omitted."
945
- })),
946
- description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Optional phase description." })),
947
- tasks: (0, _orkestrel_contract.arrayShape)(taskDraftShape, { description: "The phase tasks; they run CONCURRENTLY." }),
948
- concurrency: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
949
- min: 1,
950
- description: "Max tasks in flight at once (a resource throttle); omitted means unbounded."
951
- })),
952
- bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
953
- });
954
- /**
955
- * The shape of a DRAFT workflow — identical to {@link workflowShape} EXCEPT `id` and
956
- * `name` are OPTIONAL at all three levels (workflow / phase / task), so a small model
957
- * can omit the six identity strings and let the tool synthesize them positionally.
790
+ * The shape of a {@link import('./types.js').PhaseUpdate} a partial edit to a
791
+ * `pending` phase's `name` / `description` / `concurrency` / `bail`, all optional.
958
792
  *
959
793
  * @remarks
960
- * The lenient counterpart {@link import('./factories.js').createWorkflowDraftContract}
961
- * compiles. `run` stays required; a provided `id` / `name` still has `minLength: 1` (so an
962
- * explicitly-empty `id: ''` is REJECTED, not auto-filled). After
963
- * {@link import('./helpers.js').completeDraft} fills the missing ids/names, the result is
964
- * validated against the STRICT {@link import('./factories.js').createWorkflowContract} gate
965
- * before running.
794
+ * Mirrors {@link phaseShape}'s corresponding field constraints exactly; never `id` /
795
+ * `tasks` (structural children change through the phase's own `add` / `remove` /
796
+ * `move`, not a patch, AGENTS §12).
966
797
  */
967
- var workflowDraftShape = (0, _orkestrel_contract.objectShape)({
968
- id: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
969
- min: 1,
970
- description: "Workflow id; auto-filled when omitted."
971
- })),
798
+ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
972
799
  name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
973
800
  min: 1,
974
- description: "Workflow name; defaults to the id when omitted."
801
+ description: "New phase name."
975
802
  })),
976
- description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Optional workflow description." })),
977
- phases: (0, _orkestrel_contract.arrayShape)(phaseDraftShape, { description: "The workflow phases; they run SEQUENTIALLY, in order." }),
978
- 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
- });
980
- /**
981
- * The shape of ONE flat step — `{ name, via? }` — the building block of
982
- * {@link workflowStepsShape}.
983
- *
984
- * @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
987
- * tool expands each step into a one-task phase, in order
988
- * ({@link import('./helpers.js').expandSteps}).
989
- */
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
- });
1001
- /**
1002
- * The FLAT authoring shape `createWorkflowTool` advertises as its `parameters` — the
1003
- * simplest surface a small model can fill: `{ name?, steps: [{ name, via? }] }`.
1004
- *
1005
- * @remarks
1006
- * 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
1008
- * full {@link import('./types.js').WorkflowDefinition} — one one-task phase per step, in
1009
- * order — then validates against the STRICT
1010
- * {@link import('./factories.js').createWorkflowContract} gate. The full nested form is
1011
- * STILL accepted by the tool (it branches on the args' shape) and is documented as the
1012
- * advanced escape-hatch in the tool's description — but THIS is what `parameters` advertises.
1013
- */
1014
- var workflowStepsShape = (0, _orkestrel_contract.objectShape)({
1015
- name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
803
+ description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "New phase description." })),
804
+ concurrency: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
1016
805
  min: 1,
1017
- description: "Optional workflow name."
806
+ description: "Max tasks in flight at once (a resource throttle); omitted leaves it unchanged."
1018
807
  })),
1019
- steps: (0, _orkestrel_contract.arrayShape)(stepShape, { description: "The ordered steps to run, one after another (each becomes a one-task phase)." })
808
+ bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Per-phase failure-policy override; omitted leaves it unchanged." }))
1020
809
  });
1021
810
  //#endregion
1022
811
  //#region src/core/stores/DatabaseWorkflowStore.ts
@@ -1178,6 +967,12 @@ var MemoryWorkflowStore = class {
1178
967
  * matching event strictly AFTER the state change, BEFORE the cascade; the emitter isolates
1179
968
  * a listener throw and routes it to its `error` handler (the `error` option), so a buggy
1180
969
  * observer can never corrupt a transition.
970
+ * - **Declarative config (AGENTS §12).** `run` / `retries` / `timeout` PERSIST in a
971
+ * {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
972
+ * matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
973
+ * is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
974
+ * workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
975
+ * NEVER persisted; `undefined` when `run` is omitted or unregistered (the no-handler rule).
1181
976
  */
1182
977
  var Task = class {
1183
978
  #context;
@@ -1188,7 +983,13 @@ var Task = class {
1188
983
  #emitter;
1189
984
  #status;
1190
985
  #result;
1191
- constructor(context, phase, workflow, recompute, options, status = "pending", result) {
986
+ #name;
987
+ #description;
988
+ #run;
989
+ #retries;
990
+ #timeout;
991
+ #handler;
992
+ constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, handler) {
1192
993
  this.#context = context;
1193
994
  this.#phase = phase;
1194
995
  this.#workflow = workflow;
@@ -1200,6 +1001,12 @@ var Task = class {
1200
1001
  });
1201
1002
  this.#status = status;
1202
1003
  this.#result = result;
1004
+ this.#name = context.name;
1005
+ this.#description = context.description;
1006
+ this.#run = run;
1007
+ this.#retries = retries;
1008
+ this.#timeout = timeout;
1009
+ this.#handler = handler;
1203
1010
  }
1204
1011
  get emitter() {
1205
1012
  return this.#emitter;
@@ -1208,10 +1015,10 @@ var Task = class {
1208
1015
  return this.#context.id;
1209
1016
  }
1210
1017
  get name() {
1211
- return this.#context.name;
1018
+ return this.#name;
1212
1019
  }
1213
1020
  get description() {
1214
- return this.#context.description;
1021
+ return this.#description;
1215
1022
  }
1216
1023
  get context() {
1217
1024
  return this.#context;
@@ -1228,6 +1035,18 @@ var Task = class {
1228
1035
  get result() {
1229
1036
  return this.#result;
1230
1037
  }
1038
+ get run() {
1039
+ return this.#run;
1040
+ }
1041
+ get handler() {
1042
+ return this.#handler;
1043
+ }
1044
+ get retries() {
1045
+ return this.#retries;
1046
+ }
1047
+ get timeout() {
1048
+ return this.#timeout;
1049
+ }
1231
1050
  start() {
1232
1051
  this.#transition("running");
1233
1052
  this.#emitter.emit("start", this.id);
@@ -1262,6 +1081,29 @@ var Task = class {
1262
1081
  this.#emitter.emit("stop");
1263
1082
  this.#escalate();
1264
1083
  }
1084
+ /**
1085
+ * Apply a validated declarative patch to SELF (`name` / `description`).
1086
+ *
1087
+ * @remarks
1088
+ * Defense-in-depth (AGENTS §12): the owning
1089
+ * {@link import('../types.js').TaskManagerInterface.update} gates FIRST (target
1090
+ * exists + `pending`), so this is the second, redundant check — it THROWS a
1091
+ * `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
1092
+ *
1093
+ * @param value - The {@link TaskUpdate} fields to apply
1094
+ * @example
1095
+ * ```ts
1096
+ * task.patch({ name: 'Renamed task' })
1097
+ * ```
1098
+ */
1099
+ patch(value) {
1100
+ if (this.#status !== "pending") throw new WorkflowError("MUTATION", `task '${this.id}' cannot be patched while '${this.#status}'`, {
1101
+ task: this.id,
1102
+ status: this.#status
1103
+ });
1104
+ if (value.name !== void 0) this.#name = value.name;
1105
+ if (value.description !== void 0) this.#description = value.description;
1106
+ }
1265
1107
  snapshot() {
1266
1108
  return {
1267
1109
  id: this.id,
@@ -1269,7 +1111,10 @@ var Task = class {
1269
1111
  ...this.description === void 0 ? {} : { description: this.description },
1270
1112
  status: this.#status,
1271
1113
  ...this.#result === void 0 ? {} : { result: this.#result },
1272
- metadata: this.#metadata
1114
+ metadata: this.#metadata,
1115
+ ...this.#run === void 0 ? {} : { run: this.#run },
1116
+ ...this.#retries === void 0 ? {} : { retries: this.#retries },
1117
+ ...this.#timeout === void 0 ? {} : { timeout: this.#timeout }
1273
1118
  };
1274
1119
  }
1275
1120
  #transition(to) {
@@ -1309,6 +1154,11 @@ var Task = class {
1309
1154
  * `tasks()` lists them in positional order, `count` is the size. A `skip` is a STATUS
1310
1155
  * change on a stored task (never a removal), so order survives it; a snapshot RESTORE
1311
1156
  * re-`append`s in the snapshot's order, reproducing it exactly.
1157
+ * - **Gated mutation API (AGENTS §12).** `add` / `remove` / `move` / `update` are the
1158
+ * graceful `Result` counterparts to `append`, gating ONLY on the target's OWN
1159
+ * existence/status/id/bounds — a duplicate id, an absent/non-`pending` target, an
1160
+ * out-of-bounds `index`, or a patch that fails {@link taskUpdateShape} validation all
1161
+ * fail gracefully with a `MUTATION` {@link WorkflowError} instead of throwing.
1312
1162
  * - **No batch matrix.** A phase's tasks are a fixed positional set, so AGENTS §9.2 (the
1313
1163
  * bulk verb overloads) is deliberately omitted — there is no `remove` family here.
1314
1164
  * - **Event-free.** A purely structural container — the live {@link TaskInterface}s own
@@ -1324,18 +1174,51 @@ var Task = class {
1324
1174
  */
1325
1175
  var TaskManager = class {
1326
1176
  #tasks = /* @__PURE__ */ new Map();
1177
+ #isUpdate = (0, _orkestrel_contract.compileGuard)(taskUpdateShape);
1327
1178
  get count() {
1328
1179
  return this.#tasks.size;
1329
1180
  }
1330
1181
  append(task) {
1182
+ if (this.#tasks.has(task.id)) throw new WorkflowError("MUTATION", `duplicate task id '${task.id}'`, { id: task.id });
1331
1183
  this.#tasks.set(task.id, task);
1332
1184
  }
1185
+ add(task, index) {
1186
+ if (this.#tasks.has(task.id)) return failure(new WorkflowError("MUTATION", `duplicate task id '${task.id}'`, { id: task.id }));
1187
+ const at = index ?? this.#tasks.size;
1188
+ if (at < 0 || at > this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1189
+ this.#reorder(insertEntry([...this.#tasks.entries()], at, task.id, task));
1190
+ return success(task);
1191
+ }
1192
+ remove(id) {
1193
+ const target = this.#tasks.get(id);
1194
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1195
+ this.#tasks.delete(id);
1196
+ return success(target);
1197
+ }
1198
+ move(id, index) {
1199
+ const target = this.#tasks.get(id);
1200
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1201
+ if (index < 0 || index >= this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1202
+ this.#reorder(moveEntry([...this.#tasks.entries()], id, index));
1203
+ return success(target);
1204
+ }
1205
+ update(id, patch) {
1206
+ const target = this.#tasks.get(id);
1207
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1208
+ if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for task '${id}'`, { id }));
1209
+ target.patch(patch);
1210
+ return success(target);
1211
+ }
1333
1212
  task(id) {
1334
1213
  return this.#tasks.get(id);
1335
1214
  }
1336
1215
  tasks() {
1337
1216
  return [...this.#tasks.values()];
1338
1217
  }
1218
+ #reorder(entries) {
1219
+ this.#tasks.clear();
1220
+ for (const [key, value] of entries) this.#tasks.set(key, value);
1221
+ }
1339
1222
  };
1340
1223
  //#endregion
1341
1224
  //#region src/core/phases/Phase.ts
@@ -1360,29 +1243,59 @@ var TaskManager = class {
1360
1243
  * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE, strictly AFTER the
1361
1244
  * recompute + escalate; the emitter isolates a listener throw and routes it to its `error`
1362
1245
  * handler (the `error` option); `fail` carries the failing task's {@link TaskResult}.
1246
+ * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1247
+ * delegating to {@link tasks} (the manager gates the target's own existence/status/id/
1248
+ * bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
1249
+ * gating, purely from this phase's own derived `status` (no runner-installed hook): while
1250
+ * `pending`, any valid `index` is accepted; while `running`, `add` accepts ONLY a pure
1251
+ * append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
1252
+ * `update` always fail gracefully (the tasks are already handed to the execution
1253
+ * substrate); while terminal, everything is refused.
1254
+ * - **Patch (AGENTS §12).** `patch` applies a validated {@link PhaseUpdate} to SELF
1255
+ * (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
1256
+ * `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
1257
+ * the owning {@link WorkflowInterface.update}'s gate.
1258
+ * - **Minting (AGENTS §7).** {@link add} MINTS a live {@link Task} from a {@link TaskDefinition}
1259
+ * (converts it to a {@link TaskSnapshot}, builds the task wired to THIS phase) — the same
1260
+ * construction path {@link #append} uses at build time, so a live mint and a restored/built
1261
+ * task are wired IDENTICALLY. At construction, the workflow-level
1262
+ * {@link import('../types.js').WorkflowFunctions} registry (threaded from
1263
+ * {@link import('../types.js').WorkflowOptions.functions}) resolves each task's `run` name into
1264
+ * its runtime {@link import('../types.js').TaskInterface.handler} ONCE; a `run` that is omitted
1265
+ * or unregistered resolves to no handler (the no-handler rule).
1266
+ * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
1267
+ * quartet, scoped to this phase — a driving
1268
+ * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
1269
+ * pre-dispatch on the workflow's gate FIRST, then this phase's gate, WITHOUT touching
1270
+ * {@link status} — `paused` is runtime-only, never persisted. `skip` / `stop` (this phase's
1271
+ * own terminal forcing) always release a parked {@link wait} waiter, mirroring
1272
+ * {@link import('../Workflow.js').Workflow.destroy}'s cascade — a permanently-ended phase
1273
+ * has nothing left to pause for.
1363
1274
  */
1364
1275
  var Phase = class {
1365
- #context;
1276
+ #id;
1277
+ #name;
1278
+ #description;
1366
1279
  #workflow;
1367
1280
  #escalateUp;
1368
1281
  #tasks = new TaskManager();
1282
+ #functions;
1369
1283
  #bail;
1284
+ #concurrency;
1370
1285
  #emitter;
1371
1286
  #status;
1372
1287
  #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
- };
1288
+ #paused;
1289
+ #gate;
1290
+ constructor(snapshot, workflow, escalate, options, bail, functions) {
1291
+ this.#id = snapshot.id;
1292
+ this.#name = snapshot.name;
1293
+ this.#description = snapshot.description;
1383
1294
  this.#workflow = workflow;
1384
1295
  this.#escalateUp = escalate;
1296
+ this.#functions = functions;
1385
1297
  this.#bail = bail ?? snapshot.bail;
1298
+ this.#concurrency = snapshot.concurrency;
1386
1299
  this.#emitter = new _orkestrel_emitter.Emitter({
1387
1300
  on: options?.on,
1388
1301
  error: options?.error
@@ -1390,21 +1303,27 @@ var Phase = class {
1390
1303
  for (const task of snapshot.tasks) this.#append(task, options);
1391
1304
  this.#override = snapshot.override;
1392
1305
  this.#status = this.status;
1306
+ this.#paused = false;
1307
+ this.#gate = void 0;
1393
1308
  }
1394
1309
  get emitter() {
1395
1310
  return this.#emitter;
1396
1311
  }
1397
1312
  get id() {
1398
- return this.#context.id;
1313
+ return this.#id;
1399
1314
  }
1400
1315
  get name() {
1401
- return this.#context.name;
1316
+ return this.#name;
1402
1317
  }
1403
1318
  get description() {
1404
- return this.#context.description;
1319
+ return this.#description;
1405
1320
  }
1406
1321
  get context() {
1407
- return this.#context;
1322
+ return buildPhaseContext(this.#workflow.context, {
1323
+ id: this.#id,
1324
+ name: this.#name,
1325
+ ...this.#description === void 0 ? {} : { description: this.#description }
1326
+ });
1408
1327
  }
1409
1328
  get workflow() {
1410
1329
  return this.#workflow;
@@ -1412,6 +1331,12 @@ var Phase = class {
1412
1331
  get bail() {
1413
1332
  return this.#bail;
1414
1333
  }
1334
+ get concurrency() {
1335
+ return this.#concurrency;
1336
+ }
1337
+ get paused() {
1338
+ return this.#paused;
1339
+ }
1415
1340
  get status() {
1416
1341
  return this.#override ?? derivePhaseStatus(this.#statuses());
1417
1342
  }
@@ -1427,10 +1352,81 @@ var Phase = class {
1427
1352
  return results;
1428
1353
  }
1429
1354
  skip() {
1430
- this.#force("skipped");
1355
+ if (!isTerminalStatus(this.status)) this.#force("skipped");
1356
+ this.#paused = false;
1357
+ this.#release();
1431
1358
  }
1432
1359
  stop() {
1433
- this.#force("stopped");
1360
+ if (!isTerminalStatus(this.status)) this.#force("stopped");
1361
+ this.#paused = false;
1362
+ this.#release();
1363
+ }
1364
+ pause() {
1365
+ if (this.#paused || isTerminalStatus(this.status)) return;
1366
+ this.#paused = true;
1367
+ this.#gate = createDeferred();
1368
+ }
1369
+ resume() {
1370
+ if (!this.#paused) return;
1371
+ this.#paused = false;
1372
+ this.#release();
1373
+ }
1374
+ wait() {
1375
+ return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1376
+ }
1377
+ add(definition, index) {
1378
+ const status = this.status;
1379
+ if (isTerminalStatus(status)) return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is terminal`, {
1380
+ id: this.#id,
1381
+ status
1382
+ }));
1383
+ const created = this.#mint(definition);
1384
+ if (status === "running") {
1385
+ const at = index ?? this.#tasks.count;
1386
+ if (at !== this.#tasks.count) return failure(new WorkflowError("MUTATION", `phase '${this.#id}' only accepts an append while executing`, {
1387
+ id: this.#id,
1388
+ index: at
1389
+ }));
1390
+ return this.#addTo(created, index, at);
1391
+ }
1392
+ return this.#addTo(created, index, index ?? this.#tasks.count);
1393
+ }
1394
+ remove(id) {
1395
+ if (this.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is not pending`, {
1396
+ id: this.#id,
1397
+ status: this.status
1398
+ }));
1399
+ const result = this.#tasks.remove(id);
1400
+ if (result.success) this.#emitter.emit("remove", result.value);
1401
+ return result;
1402
+ }
1403
+ move(id, index) {
1404
+ if (this.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is not pending`, {
1405
+ id: this.#id,
1406
+ status: this.status
1407
+ }));
1408
+ const result = this.#tasks.move(id, index);
1409
+ if (result.success) this.#emitter.emit("move", result.value, index);
1410
+ return result;
1411
+ }
1412
+ update(id, patch) {
1413
+ if (this.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${this.#id}' is not pending`, {
1414
+ id: this.#id,
1415
+ status: this.status
1416
+ }));
1417
+ const result = this.#tasks.update(id, patch);
1418
+ if (result.success) this.#emitter.emit("update", result.value);
1419
+ return result;
1420
+ }
1421
+ patch(value) {
1422
+ if (this.status !== "pending") throw new WorkflowError("MUTATION", `phase '${this.#id}' can only be patched while pending`, {
1423
+ id: this.#id,
1424
+ status: this.status
1425
+ });
1426
+ if (value.name !== void 0) this.#name = value.name;
1427
+ if (value.description !== void 0) this.#description = value.description;
1428
+ if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
1429
+ if (value.bail !== void 0) this.#bail = value.bail;
1434
1430
  }
1435
1431
  snapshot() {
1436
1432
  return {
@@ -1440,6 +1436,7 @@ var Phase = class {
1440
1436
  status: this.status,
1441
1437
  ...this.#override === void 0 ? {} : { override: this.#override },
1442
1438
  bail: this.#bail,
1439
+ ...this.#concurrency === void 0 ? {} : { concurrency: this.#concurrency },
1443
1440
  tasks: this.#tasks.tasks().map((task) => task.snapshot())
1444
1441
  };
1445
1442
  }
@@ -1464,16 +1461,32 @@ var Phase = class {
1464
1461
  else if (status === "stopped") this.#emitter.emit("stop");
1465
1462
  }
1466
1463
  #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`);
1464
+ const found = findFailure(this.results());
1465
+ if (found === void 0) throw new Error(`phase '${this.id}' derived failed with no failing task result`);
1466
+ return found;
1467
+ }
1468
+ #release() {
1469
+ if (this.#gate === void 0) return;
1470
+ this.#gate.resolve();
1471
+ this.#gate = void 0;
1472
+ }
1473
+ #addTo(task, index, at) {
1474
+ const result = this.#tasks.add(task, index);
1475
+ if (result.success) this.#emitter.emit("add", result.value, at);
1476
+ return result;
1472
1477
  }
1473
1478
  #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);
1479
+ const created = this.#create(task, options?.tasks?.[task.id]);
1475
1480
  this.#tasks.append(created);
1476
1481
  }
1482
+ #create(snapshot, options) {
1483
+ const context = buildTaskContext(this.context, snapshot);
1484
+ const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
1485
+ return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, handler);
1486
+ }
1487
+ #mint(definition) {
1488
+ return this.#create(taskDefinitionToSnapshot(definition), void 0);
1489
+ }
1477
1490
  #statuses() {
1478
1491
  return this.#tasks.tasks().map((task) => task.status);
1479
1492
  }
@@ -1490,6 +1503,11 @@ var Phase = class {
1490
1503
  * `append` adds one at the end, `phase(id)` looks one up, `phases()` lists them in
1491
1504
  * positional order, `count` is the size. A snapshot RESTORE re-`append`s in the
1492
1505
  * snapshot's order, reproducing it exactly.
1506
+ * - **Gated mutation API (AGENTS §12).** `add` / `remove` / `move` / `update` are the
1507
+ * graceful `Result` counterparts to `append`, gating ONLY on the target's OWN
1508
+ * existence/status/id/bounds — a duplicate id, an absent/non-`pending` target, an
1509
+ * out-of-bounds `index`, or a patch that fails {@link phaseUpdateShape} validation
1510
+ * all fail gracefully with a `MUTATION` {@link WorkflowError} instead of throwing.
1493
1511
  * - **No batch matrix.** A workflow's phases are a fixed positional set, so AGENTS §9.2
1494
1512
  * is deliberately omitted.
1495
1513
  * - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own
@@ -1505,18 +1523,51 @@ var Phase = class {
1505
1523
  */
1506
1524
  var PhaseManager = class {
1507
1525
  #phases = /* @__PURE__ */ new Map();
1526
+ #isUpdate = (0, _orkestrel_contract.compileGuard)(phaseUpdateShape);
1508
1527
  get count() {
1509
1528
  return this.#phases.size;
1510
1529
  }
1511
1530
  append(phase) {
1531
+ if (this.#phases.has(phase.id)) throw new WorkflowError("MUTATION", `duplicate phase id '${phase.id}'`, { id: phase.id });
1512
1532
  this.#phases.set(phase.id, phase);
1513
1533
  }
1534
+ add(phase, index) {
1535
+ if (this.#phases.has(phase.id)) return failure(new WorkflowError("MUTATION", `duplicate phase id '${phase.id}'`, { id: phase.id }));
1536
+ const at = index ?? this.#phases.size;
1537
+ if (at < 0 || at > this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1538
+ this.#reorder(insertEntry([...this.#phases.entries()], at, phase.id, phase));
1539
+ return success(phase);
1540
+ }
1541
+ remove(id) {
1542
+ const target = this.#phases.get(id);
1543
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
1544
+ this.#phases.delete(id);
1545
+ return success(target);
1546
+ }
1547
+ move(id, index) {
1548
+ const target = this.#phases.get(id);
1549
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
1550
+ if (index < 0 || index >= this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1551
+ this.#reorder(moveEntry([...this.#phases.entries()], id, index));
1552
+ return success(target);
1553
+ }
1554
+ update(id, patch) {
1555
+ const target = this.#phases.get(id);
1556
+ if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
1557
+ if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for phase '${id}'`, { id }));
1558
+ target.patch(patch);
1559
+ return success(target);
1560
+ }
1514
1561
  phase(id) {
1515
1562
  return this.#phases.get(id);
1516
1563
  }
1517
1564
  phases() {
1518
1565
  return [...this.#phases.values()];
1519
1566
  }
1567
+ #reorder(entries) {
1568
+ this.#phases.clear();
1569
+ for (const [key, value] of entries) this.#phases.set(key, value);
1570
+ }
1520
1571
  };
1521
1572
  //#endregion
1522
1573
  //#region src/core/Workflow.ts
@@ -1547,27 +1598,52 @@ var PhaseManager = class {
1547
1598
  * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE; the emitter isolates a
1548
1599
  * listener throw and routes it to its `error` handler (the `error` option); `fail` carries
1549
1600
  * the failing task's {@link TaskResult}.
1601
+ * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1602
+ * delegating to {@link phases} (the manager gates the target's own existence/status/id/
1603
+ * bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
1604
+ * bottom-up gating (no runner-installed hook): refused outright while this workflow's own
1605
+ * `status` is terminal; otherwise a target position must fall within the PENDING SUFFIX —
1606
+ * the contiguous trailing run of `pending` phases — whose boundary is
1607
+ * {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
1608
+ * workflow's phases are all `pending`, so the boundary is `0` and every position is
1609
+ * naturally accepted.
1610
+ * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
1611
+ * phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
1612
+ * persisted. `destroy` is a terminal teardown: it aborts {@link signal}, `stop`s every
1613
+ * non-terminal live phase (so an engine parked on a phase's own gate unparks and the tree
1614
+ * lands coherent), forces the `stop` override on THIS workflow when not already terminal,
1615
+ * releases any parked {@link wait} waiter, and marks {@link destroyed} — all four idempotent.
1550
1616
  */
1551
1617
  var Workflow = class {
1552
1618
  #context;
1553
1619
  #bail;
1554
1620
  #bailOverride;
1621
+ #functions;
1555
1622
  #phases = new PhaseManager();
1556
1623
  #emitter;
1557
1624
  #created;
1558
1625
  #updated;
1559
1626
  #status;
1560
1627
  #override;
1628
+ #abort;
1629
+ #paused;
1630
+ #gate;
1631
+ #destroyed;
1561
1632
  constructor(snapshot, options) {
1562
1633
  this.#context = buildWorkflowContext(snapshot);
1563
1634
  this.#bail = options?.bail ?? snapshot.bail;
1564
1635
  this.#bailOverride = options?.bail;
1636
+ this.#functions = options?.functions;
1565
1637
  this.#emitter = new _orkestrel_emitter.Emitter({
1566
1638
  on: options?.on,
1567
1639
  error: options?.error
1568
1640
  });
1569
1641
  this.#created = snapshot.created;
1570
1642
  this.#updated = snapshot.updated;
1643
+ this.#abort = (0, _orkestrel_abort.createAbort)();
1644
+ this.#paused = false;
1645
+ this.#gate = void 0;
1646
+ this.#destroyed = false;
1571
1647
  for (const phase of snapshot.phases) this.#append(phase, options);
1572
1648
  this.#override = snapshot.override;
1573
1649
  this.#status = this.status;
@@ -1590,6 +1666,15 @@ var Workflow = class {
1590
1666
  get bail() {
1591
1667
  return this.#bail;
1592
1668
  }
1669
+ get paused() {
1670
+ return this.#paused;
1671
+ }
1672
+ get destroyed() {
1673
+ return this.#destroyed;
1674
+ }
1675
+ get signal() {
1676
+ return this.#abort.signal;
1677
+ }
1593
1678
  get status() {
1594
1679
  return this.#override ?? deriveWorkflowStatus(this.#statuses());
1595
1680
  }
@@ -1603,13 +1688,95 @@ var Workflow = class {
1603
1688
  return collectResults(this.#phases.phases().map((phase) => phase.results()));
1604
1689
  }
1605
1690
  skip() {
1606
- this.#force("skipped");
1691
+ if (!isTerminalStatus(this.status)) this.#force("skipped");
1692
+ this.#paused = false;
1693
+ this.#release();
1607
1694
  }
1608
1695
  stop() {
1609
- this.#force("stopped");
1696
+ if (!isTerminalStatus(this.status)) this.#force("stopped");
1697
+ this.#paused = false;
1698
+ this.#release();
1610
1699
  }
1611
1700
  complete() {
1612
- this.#force("completed");
1701
+ if (this.status === "pending") this.#force("completed");
1702
+ }
1703
+ pause() {
1704
+ if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
1705
+ this.#paused = true;
1706
+ this.#gate = createDeferred();
1707
+ }
1708
+ resume() {
1709
+ if (!this.#paused) return;
1710
+ this.#paused = false;
1711
+ this.#release();
1712
+ }
1713
+ destroy() {
1714
+ if (this.#destroyed) return;
1715
+ this.#destroyed = true;
1716
+ this.#abort.abort();
1717
+ for (const phase of this.#phases.phases()) if (!isTerminalStatus(phase.status)) phase.stop();
1718
+ if (!isTerminalStatus(this.status)) this.stop();
1719
+ this.#paused = false;
1720
+ this.#release();
1721
+ }
1722
+ wait() {
1723
+ return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1724
+ }
1725
+ add(definition, index) {
1726
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
1727
+ id: this.id,
1728
+ status: this.status
1729
+ }));
1730
+ const at = index ?? this.#phases.count;
1731
+ if (at < this.#boundary()) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' add index precedes boundary`, {
1732
+ id: this.id,
1733
+ index: at
1734
+ }));
1735
+ return this.#addTo(this.#mint(definition), index, at);
1736
+ }
1737
+ remove(id) {
1738
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
1739
+ id: this.id,
1740
+ status: this.status
1741
+ }));
1742
+ const at = this.#indexOf(id);
1743
+ if (at === -1 || at < this.#boundary()) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' cannot remove '${id}'`, {
1744
+ id: this.id,
1745
+ phase: id
1746
+ }));
1747
+ const result = this.#phases.remove(id);
1748
+ if (result.success) this.#emitter.emit("remove", result.value);
1749
+ return result;
1750
+ }
1751
+ move(id, index) {
1752
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
1753
+ id: this.id,
1754
+ status: this.status
1755
+ }));
1756
+ const at = this.#indexOf(id);
1757
+ const boundary = this.#boundary();
1758
+ if (at === -1 || at < boundary || index < boundary) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' cannot move '${id}'`, {
1759
+ id: this.id,
1760
+ phase: id,
1761
+ index
1762
+ }));
1763
+ const result = this.#phases.move(id, index);
1764
+ if (result.success) this.#emitter.emit("move", result.value, index);
1765
+ return result;
1766
+ }
1767
+ update(id, patch) {
1768
+ if (isTerminalStatus(this.status)) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' is terminal`, {
1769
+ id: this.id,
1770
+ status: this.status
1771
+ }));
1772
+ const at = this.#indexOf(id);
1773
+ if (at === -1 || at < this.#boundary()) return failure(new WorkflowError("MUTATION", `workflow '${this.id}' cannot update '${id}'`, {
1774
+ id: this.id,
1775
+ phase: id
1776
+ }));
1777
+ const result = this.#phases.update(id, patch);
1778
+ if (result.success) this.#emitter.emit("update", result.value);
1779
+ return result;
1613
1780
  }
1614
1781
  snapshot() {
1615
1782
  return {
@@ -1641,14 +1808,34 @@ var Workflow = class {
1641
1808
  else if (status === "failed") this.#emitter.emit("fail", this.#failure());
1642
1809
  else if (status === "stopped") this.#emitter.emit("stop");
1643
1810
  }
1811
+ #addTo(phase, index, at) {
1812
+ const result = this.#phases.add(phase, index);
1813
+ if (result.success) this.#emitter.emit("add", result.value, at);
1814
+ return result;
1815
+ }
1816
+ #indexOf(id) {
1817
+ return this.#phases.phases().findIndex((phase) => phase.id === id);
1818
+ }
1819
+ #boundary() {
1820
+ return deriveBoundary(this.#phases.phases().map((phase) => phase.status));
1821
+ }
1644
1822
  #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`);
1823
+ const found = findFailure(this.results());
1824
+ if (found === void 0) throw new Error(`workflow '${this.id}' derived failed with no failing task result`);
1825
+ return found;
1647
1826
  }
1648
1827
  #append(phase, options) {
1649
- const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride);
1828
+ const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride, this.#functions);
1650
1829
  this.#phases.append(created);
1651
1830
  }
1831
+ #mint(definition) {
1832
+ return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions);
1833
+ }
1834
+ #release() {
1835
+ if (this.#gate === void 0) return;
1836
+ this.#gate.resolve();
1837
+ this.#gate = void 0;
1838
+ }
1652
1839
  #statuses() {
1653
1840
  return this.#phases.phases().map((phase) => ({
1654
1841
  status: phase.status,
@@ -1701,10 +1888,7 @@ var Controller = class {
1701
1888
  return this.#abort.aborted;
1702
1889
  }
1703
1890
  wait() {
1704
- if (this.signal.aborted) return Promise.resolve();
1705
- return new Promise((resolve) => {
1706
- this.signal.addEventListener("abort", () => resolve(), { once: true });
1707
- });
1891
+ return parkSignal(this.signal);
1708
1892
  }
1709
1893
  spawn(input) {
1710
1894
  return this.#spawn(input);
@@ -1749,6 +1933,15 @@ var Controller = class {
1749
1933
  * unit failure (after its retries) records the error and `abort()`s the run, so every
1750
1934
  * sibling's signal fires; later failures are ignored and `execute` rejects with the
1751
1935
  * first error. A user `abort(reason)` likewise rejects a running `execute`.
1936
+ * - **`pause` / `resume` / `stop` (§10) ride the backing Queue.** `pause` / `resume`
1937
+ * delegate straight to the Queue's own pause/resume (holding/releasing the NEXT
1938
+ * dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
1939
+ * GRACEFUL permanent end, distinct from `abort`: still-pending (never-dispatched)
1940
+ * units are rejected by the Queue's own stop WITHOUT their handler ever running, and
1941
+ * `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
1942
+ * not a failure, never tripping fail-fast — while an in-flight unit still runs to
1943
+ * completion and settles normally. `execute` RESOLVES (never rejects) once every unit
1944
+ * has settled, with whatever results actually completed.
1752
1945
  * - **Observable (§13).** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
1753
1946
  * lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
1754
1947
  * fire-and-forget observers. Every event is emitted directly, strictly AFTER the relevant
@@ -1766,11 +1959,13 @@ var Runner = class {
1766
1959
  #aborts = /* @__PURE__ */ new Map();
1767
1960
  #order = [];
1768
1961
  #values = /* @__PURE__ */ new Map();
1962
+ #dispatched = /* @__PURE__ */ new Set();
1769
1963
  #count = 0;
1770
1964
  #drained;
1771
1965
  #started = false;
1772
1966
  #running = false;
1773
1967
  #stopped = false;
1968
+ #stopping = false;
1774
1969
  #failure;
1775
1970
  constructor(options) {
1776
1971
  this.#handler = options.handler;
@@ -1795,6 +1990,39 @@ var Runner = class {
1795
1990
  get stopped() {
1796
1991
  return this.#stopped;
1797
1992
  }
1993
+ get paused() {
1994
+ return this.#queue.paused;
1995
+ }
1996
+ /**
1997
+ * Inject one more unit into an IN-FLIGHT `execute` run — a LIVE counterpart to a
1998
+ * `Controller.spawn`, called from OUTSIDE any unit's handler.
1999
+ *
2000
+ * @remarks
2001
+ * Returns `undefined` synchronously (graceful, non-throwing — AGENTS §12) unless the
2002
+ * runner is currently mid-`execute` and not yet stopped — covering "never started",
2003
+ * "already drained", "aborted", and "destroyed". Otherwise the unit is routed through
2004
+ * the SAME backing queue as a declared/`spawn`ed unit via `#launch` — the outstanding-
2005
+ * unit count gate increments BEFORE this call returns, so an in-flight `execute`
2006
+ * keeps awaiting it (the drain race: `#running` flips to `false` as the very first
2007
+ * step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
2008
+ * method after the run has fully drained is cleanly rejected with `undefined` —
2009
+ * never silently dropped, never hangs `execute`). Emits {@link RunnerEventMap.spawn}
2010
+ * with a `parent` of `undefined` (this call has no spawning unit) once accepted.
2011
+ *
2012
+ * @param input - The unit's work payload
2013
+ * @returns The unit's result promise, or `undefined` when no in-flight run can accept it
2014
+ * @example
2015
+ * ```ts
2016
+ * const runner = createRunner({ handler: (c) => c.input })
2017
+ * const result = runner.execute([1, 2])
2018
+ * const extra = runner.spawn(3) // Promise<number> | undefined
2019
+ * await result
2020
+ * ```
2021
+ */
2022
+ spawn(input) {
2023
+ if (this.#stopped || !this.#running) return void 0;
2024
+ return this.#launch(input, void 0, true);
2025
+ }
1798
2026
  async execute(inputs) {
1799
2027
  if (this.#started) throw new Error("runner has already executed");
1800
2028
  if (this.#stopped) throw new Error("runner is stopped");
@@ -1824,6 +2052,47 @@ var Runner = class {
1824
2052
  this.#stopped = true;
1825
2053
  this.#emitter.emit("abort", reason);
1826
2054
  }
2055
+ /**
2056
+ * Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
2057
+ * `pause`, which holds the NEXT dispatch while any in-flight unit finishes.
2058
+ *
2059
+ * @remarks
2060
+ * A no-op once the runner is `stopped` — a stopped runner has no dispatch left to
2061
+ * suspend, mirroring the guard `stop()` itself applies. Also a no-op when already
2062
+ * `paused` (the queue's own `pause` is idempotent), so calling it repeatedly is safe.
2063
+ */
2064
+ pause() {
2065
+ if (this.#stopped || this.#queue.paused) return;
2066
+ this.#queue.pause();
2067
+ }
2068
+ /**
2069
+ * Continue a paused runner (AGENTS §10); delegates to the backing queue's `resume`.
2070
+ *
2071
+ * @remarks
2072
+ * A no-op once the runner is `stopped` (nothing left to resume) and a no-op when the
2073
+ * runner is not currently `paused`, so calling it repeatedly or on a never-paused
2074
+ * runner is safe.
2075
+ */
2076
+ resume() {
2077
+ if (this.#stopped || !this.#queue.paused) return;
2078
+ this.#queue.resume();
2079
+ }
2080
+ /**
2081
+ * Permanently end the runner (AGENTS §10) — a GRACEFUL stop, distinct from `abort`.
2082
+ * Marks the runner `stopping` + `stopped`, then stops the backing queue: every
2083
+ * still-PENDING (never-dispatched) unit is rejected by the queue with its own
2084
+ * "queue is stopped" error, WITHOUT running its handler; every already-in-flight unit
2085
+ * keeps running to completion and settles normally. `#settle` reads `#stopping` to
2086
+ * classify a never-dispatched unit's rejection as a stop artifact (decrement the count
2087
+ * gate, no recorded failure, no fail-fast trip) rather than a genuine failure — a
2088
+ * dispatched unit's rejection while stopping is still a real failure. Idempotent.
2089
+ */
2090
+ stop() {
2091
+ if (this.#stopped) return;
2092
+ this.#stopping = true;
2093
+ this.#stopped = true;
2094
+ this.#queue.stop();
2095
+ }
1827
2096
  destroy() {
1828
2097
  if (this.#stopped) {
1829
2098
  this.#queue.destroy();
@@ -1832,13 +2101,13 @@ var Runner = class {
1832
2101
  this.abort();
1833
2102
  this.#queue.destroy();
1834
2103
  }
1835
- #launch(input, parent) {
2104
+ #launch(input, parent, announce = parent !== void 0) {
1836
2105
  const id = crypto.randomUUID();
1837
2106
  const abort = (0, _orkestrel_abort.createAbort)();
1838
2107
  this.#aborts.set(id, abort);
1839
2108
  this.#order.push(id);
1840
2109
  this.#count += 1;
1841
- if (parent !== void 0) this.#emitter.emit("spawn", id, parent);
2110
+ if (announce) this.#emitter.emit("spawn", id, parent);
1842
2111
  const promise = this.#queue.enqueue({
1843
2112
  id,
1844
2113
  input
@@ -1859,6 +2128,7 @@ var Runner = class {
1859
2128
  #dispatch(unit, execution) {
1860
2129
  const abort = this.#aborts.get(unit.id);
1861
2130
  if (abort === void 0) throw new Error("unit abort missing");
2131
+ this.#dispatched.add(unit.id);
1862
2132
  const controller = new Controller(unit.id, unit.input, abort, execution.signal, (input) => this.#spawn(input, unit.id));
1863
2133
  this.#emitter.emit("unit", unit.id);
1864
2134
  return this.#handler(controller);
@@ -1871,7 +2141,7 @@ var Runner = class {
1871
2141
  if (outcome.ok) {
1872
2142
  this.#values.set(id, { value: outcome.value });
1873
2143
  this.#emitter.emit("settle", id);
1874
- } else if (this.#failure === void 0) {
2144
+ } else if (this.#stopping && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
1875
2145
  this.#failure = { error: outcome.error };
1876
2146
  this.#emitter.emit("fail", id, outcome.error);
1877
2147
  this.abort(outcome.error);
@@ -1939,35 +2209,51 @@ var TaskController = class {
1939
2209
  //#region src/core/WorkflowRunner.ts
1940
2210
  /**
1941
2211
  * 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.
2212
+ * substrate — phases sequential, tasks concurrent — dispatching each task through its OWN
2213
+ * resolved handler under the `bail` policy.
1944
2214
  *
1945
2215
  * @remarks
1946
2216
  * - **Composes, never re-implements.** Per-phase bounded concurrency is one
1947
2217
  * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
1948
2218
  * `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} +
2219
+ * timeout / budget / entity `signal` fold through {@link createAbort} / {@link createTimeout} +
1950
2220
  * `AbortSignal.any` (exactly as the agent runtime folds its bounds); pacing is the shipped
1951
2221
  * {@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.
2222
+ * its own — it only sequences phases, dispatches a task's own handler, and drives the live
2223
+ * entity.
2224
+ * - **Pure engine no registries, no tool/agent knowledge.** The runner carries no
2225
+ * `functions` / `tools` / `agents` registry: each live {@link TaskInterface} already
2226
+ * resolved its own {@link import('./types.js').WorkflowFunction} into
2227
+ * {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
2228
+ * or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
2229
+ * dispatch is simply "invoke the task's own handler". Static tool / agent calling is an
2230
+ * OPT-IN concern of the `@orkestrel/tool` package's adapter factories — plain
2231
+ * {@link import('./types.js').WorkflowFunction}s a caller wires into
2232
+ * {@link WorkflowOptions.functions} like any other behavior. This module never imports
2233
+ * any tool/agent package.
2234
+ * - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
2235
+ * from a {@link WorkflowDefinition} (single source of truth for the `run` / `concurrency`
2236
+ * metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
2237
+ * {@link WorkflowInterface} instead the entity-native control surface (AGENTS §10:
2238
+ * `pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
2239
+ * converge on the SAME `#execute` engine: neither reads a `WorkflowDefinition` once the tree
2240
+ * exists `#runTask` reads each task's OWN {@link import('./types.js').TaskInterface.handler}
2241
+ * / `retries` / `timeout`, and `#runPhase` reads each phase's OWN
2242
+ * {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
2243
+ * runs under EXACTLY the same rules as one built from the original definition.
2244
+ * - **Phases sequential, tasks concurrent — LIVE continuity.** `#execute` drives the phases in
2245
+ * order, RE-READING `workflow.phases.phases()` every iteration (a cursor over the live
2246
+ * manager, not a one-time snapshot) so a caller's `workflow.add(phaseDefinition)` mid-run is
2247
+ * picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event BEFORE
2248
+ * capturing its task list, then `spawn`s any task added mid-phase onto the SAME substrate
2249
+ * Runner (so it is actually dispatched, under the same `concurrency`); a task added too late
2250
+ * for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
2251
+ * phase always reaches a coherent terminal state.
2252
+ * - **Dispatch by handler.** `#runTask` invokes the live task's own
2253
+ * {@link import('./types.js').TaskInterface.handler} directly: `undefined` (an omitted `run`,
2254
+ * or a `run` name absent from the {@link WorkflowOptions.functions} registry it was resolved
2255
+ * against) AUTO-COMPLETES — the ROADMAP no-handler rule; otherwise the handler runs with the
2256
+ * task's {@link import('./types.js').TaskControllerInterface} handle.
1971
2257
  * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
1972
2258
  * THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
1973
2259
  * (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
@@ -1975,7 +2261,20 @@ var TaskController = class {
1975
2261
  * Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
1976
2262
  * the Runner settles every unit (allSettled) and the run finishes (the workflow derives
1977
2263
  * `completed`, the failure recorded in the result tree).
1978
- * - **Abort / Timeout / Budget fold.** `#execute` folds the run's external `signal`, a
2264
+ * - **Pause / stop / destroy gates.** `workflow.pause()` is honoured at exactly two points —
2265
+ * the next phase boundary (workflow-only) and each task's own pre-dispatch (before
2266
+ * `task.start()`, workflow gate FIRST then this task's own `phase.pause()`) — by parking on
2267
+ * {@link WorkflowInterface.wait} / {@link PhaseInterface.wait}; an in-flight task body is
2268
+ * NEVER suspended mid-flight. A GRACEFUL `workflow.stop()` (no signal involved) is caught at
2269
+ * those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
2270
+ * HARD `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
2271
+ * into the run's composed signal — so it cancels the active phase Runner (and every
2272
+ * in-flight task) exactly like an external abort / timeout / budget fire. EVERY park on a
2273
+ * `wait()` gate is RACED against that same run signal (`#raceWait`, S2) — so a cancel firing
2274
+ * WHILE parked unparks the engine promptly instead of hanging until `resume`; the existing
2275
+ * halt / abort re-checks after the gate then decide the outcome.
2276
+ * - **Abort / Timeout / Budget / entity-signal fold.** `#execute` folds the live workflow's
2277
+ * own {@link WorkflowInterface.signal}, the run's external `signal`, a
1979
2278
  * {@link TimeoutInterface}, and the `@orkestrel/budget` package's `BudgetInterface`'s
1980
2279
  * `signal` into one `runSignal` (`AbortSignal.any`); a fire aborts the active phase's Runner
1981
2280
  * (cancelling every in-flight task) and HALTS the run — the remaining tasks / phases `skip`
@@ -1983,59 +2282,69 @@ var TaskController = class {
1983
2282
  * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
1984
2283
  * `runSignal`, so a handler observes either cause directly.
1985
2284
  * - **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.
2285
+ * each `#execute`, so a nested `execute` (a bound workflow-tool handler re-entering this
2286
+ * instance while the outer run is suspended awaiting it) cannot clobber the outer run's state.
1988
2287
  */
1989
2288
  var WorkflowRunner = class {
1990
- #functions;
1991
- #tools;
1992
- #agents;
1993
2289
  #scheduler;
1994
- #workflowTool;
1995
- constructor(functions, tools, agents, scheduler, workflowTool) {
1996
- this.#functions = functions;
1997
- this.#tools = tools;
1998
- this.#agents = agents;
2290
+ constructor(scheduler) {
1999
2291
  this.#scheduler = scheduler;
2000
- this.#workflowTool = workflowTool;
2001
2292
  }
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);
2293
+ execute(target, options) {
2294
+ if (this.#isWorkflow(target)) {
2295
+ if (target.status !== "pending" || target.destroyed) throw new WorkflowError("TRANSITION", `workflow '${target.id}' is not drivable`, {
2296
+ id: target.id,
2297
+ status: target.status,
2298
+ destroyed: target.destroyed
2299
+ });
2300
+ return this.#execute(target, options);
2301
+ }
2302
+ const workflow = new Workflow(definitionToSnapshot(target, options?.bail ?? target.bail ?? false), options);
2303
+ return this.#execute(workflow, options);
2007
2304
  }
2008
- async #execute(workflow, definition, options, depth, ancestry) {
2305
+ async #execute(workflow, options) {
2009
2306
  const ms = options?.timeout;
2010
2307
  const timeout = ms !== void 0 && ms > 0 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
2011
2308
  timeout?.start();
2012
2309
  options?.budget?.start();
2013
- const runSignal = this.#fold(options, timeout);
2310
+ const runSignal = this.#fold(workflow, options, timeout);
2014
2311
  const holder = { runner: void 0 };
2015
- const onCancel = () => holder.runner?.abort(runSignal?.reason);
2016
- if (runSignal !== void 0) if (runSignal.aborted) onCancel();
2312
+ const onCancel = () => holder.runner?.abort(runSignal.reason);
2313
+ if (runSignal.aborted) onCancel();
2017
2314
  else runSignal.addEventListener("abort", onCancel, { once: true });
2018
2315
  try {
2019
- const phases = workflow.phases.phases();
2020
- for (let index = 0; index < phases.length; index += 1) {
2316
+ let index = 0;
2317
+ for (;;) {
2318
+ const phases = workflow.phases.phases();
2319
+ if (index >= phases.length) break;
2021
2320
  const phase = phases[index];
2022
- if (phase === void 0) continue;
2321
+ if (phase === void 0) {
2322
+ index += 1;
2323
+ continue;
2324
+ }
2325
+ if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2326
+ this.#haltFrom(phases, index, workflow, runSignal);
2327
+ break;
2328
+ }
2329
+ if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
2023
2330
  if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2024
- this.#skipFrom(phases, index);
2331
+ this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
2025
2332
  break;
2026
2333
  }
2027
- if (await this.#runPhase(workflow, phase, this.#phaseOf(definition, phase.id), runSignal, holder, depth, ancestry)) {
2028
- this.#skipFrom(phases, index + 1);
2334
+ if (await this.#runPhase(workflow, phase, runSignal, holder)) {
2335
+ this.#skipFrom(workflow.phases.phases(), index + 1);
2029
2336
  break;
2030
2337
  }
2031
- if (index < phases.length - 1 && !this.#cancelled(runSignal)) try {
2032
- await this.#scheduler.yield(runSignal === void 0 ? void 0 : { signal: runSignal });
2033
- } catch {}
2338
+ index += 1;
2339
+ const remaining = workflow.phases.phases();
2340
+ if (index < remaining.length && !this.#cancelled(runSignal)) try {
2341
+ await this.#scheduler.yield({ signal: runSignal });
2342
+ } catch (error) {
2343
+ if (!runSignal.aborted) throw error;
2344
+ }
2034
2345
  }
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();
2346
+ if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
2347
+ else if (this.#completable(workflow)) workflow.complete();
2039
2348
  return {
2040
2349
  workflow,
2041
2350
  status: workflow.status,
@@ -2043,52 +2352,71 @@ var WorkflowRunner = class {
2043
2352
  };
2044
2353
  } finally {
2045
2354
  timeout?.clear();
2046
- runSignal?.removeEventListener("abort", onCancel);
2355
+ runSignal.removeEventListener("abort", onCancel);
2047
2356
  }
2048
2357
  }
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;
2358
+ async #runPhase(workflow, phase, runSignal, holder) {
2359
+ const launched = /* @__PURE__ */ new Set();
2360
+ let runner;
2361
+ const onAdd = (task) => {
2362
+ if (launched.has(task.id)) return;
2363
+ launched.add(task.id);
2364
+ runner?.spawn(task);
2365
+ };
2366
+ phase.emitter.on("add", onAdd);
2067
2367
  try {
2068
- await runner.execute(tasks);
2069
- return false;
2070
- } catch {
2071
- return !this.#cancelled(runSignal);
2368
+ const tasks = phase.tasks.tasks();
2369
+ for (const task of tasks) launched.add(task.id);
2370
+ if (tasks.length === 0) return false;
2371
+ const bail = phase.bail;
2372
+ const concurrency = phase.concurrency !== void 0 && phase.concurrency > 0 ? phase.concurrency : DEFAULT_PHASE_CONCURRENCY;
2373
+ const attempts = /* @__PURE__ */ new Map();
2374
+ const created = new Runner({
2375
+ concurrency,
2376
+ entries: (task) => ({
2377
+ retries: task.retries,
2378
+ timeout: task.timeout
2379
+ }),
2380
+ handler: (controller) => this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts)
2381
+ });
2382
+ runner = created;
2383
+ holder.runner = created;
2384
+ try {
2385
+ await created.execute(tasks);
2386
+ return false;
2387
+ } catch {
2388
+ return !this.#cancelled(runSignal);
2389
+ } finally {
2390
+ created.destroy();
2391
+ holder.runner = void 0;
2392
+ }
2072
2393
  } finally {
2073
- runner.destroy();
2074
- holder.runner = void 0;
2394
+ phase.emitter.off("add", onAdd);
2395
+ if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2396
+ for (const task of phase.tasks.tasks()) this.#skip(task);
2075
2397
  }
2076
2398
  }
2077
- async #runTask(workflow, task, definition, controller, runSignal, bail, attempts, depth, ancestry) {
2399
+ async #runTask(workflow, task, controller, runSignal, bail, attempts) {
2078
2400
  const signal = this.#taskSignal(controller.signal, runSignal);
2079
2401
  const attempt = (attempts.get(task.id) ?? 0) + 1;
2080
2402
  attempts.set(task.id, attempt);
2081
- const last = attempt > Math.max(0, definition?.retries ?? 0);
2403
+ const last = attempt > Math.max(0, task.retries ?? 0);
2404
+ if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
2405
+ if (task.phase.paused) await this.#raceWait(() => task.phase.wait(), runSignal);
2406
+ if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2407
+ this.#skipCancelled(task, workflow, runSignal);
2408
+ return;
2409
+ }
2082
2410
  if (task.status === "pending") task.start();
2083
- if (this.#skipping(controller, runSignal)) {
2084
- this.#skip(task);
2411
+ if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2412
+ this.#skipCancelled(task, workflow, runSignal);
2085
2413
  return;
2086
2414
  }
2087
2415
  const handle = new TaskController(signal, task.snapshot().metadata, task.context, () => workflow.results());
2088
2416
  try {
2089
- const value = await this.#dispatch(definition, handle, depth, ancestry);
2417
+ const value = task.handler === void 0 ? void 0 : await task.handler(handle);
2090
2418
  if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2091
- this.#skip(task);
2419
+ this.#skipCancelled(task, workflow, runSignal);
2092
2420
  return;
2093
2421
  }
2094
2422
  if (signal.aborted) {
@@ -2098,7 +2426,7 @@ var WorkflowRunner = class {
2098
2426
  task.complete(value);
2099
2427
  } catch (error) {
2100
2428
  if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2101
- this.#skip(task);
2429
+ this.#skipCancelled(task, workflow, runSignal);
2102
2430
  return;
2103
2431
  }
2104
2432
  if (signal.aborted) {
@@ -2114,80 +2442,32 @@ var WorkflowRunner = class {
2114
2442
  if (!last) return;
2115
2443
  task.fail(/* @__PURE__ */ new Error(`task '${task.id}' timed out`));
2116
2444
  }
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]
2445
+ async #raceWait(wait, runSignal) {
2446
+ if (runSignal.aborted) return;
2447
+ let onAbort;
2448
+ const cancelled = new Promise((resolve) => {
2449
+ onAbort = () => resolve();
2450
+ runSignal.addEventListener("abort", onAbort, { once: true });
2152
2451
  });
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
2452
  try {
2174
- return await agent.generate();
2453
+ await Promise.race([wait(), cancelled]);
2175
2454
  } finally {
2176
- signal.removeEventListener("abort", onAbort);
2455
+ if (onAbort !== void 0) runSignal.removeEventListener("abort", onAbort);
2177
2456
  }
2178
2457
  }
2179
2458
  #taskSignal(unitSignal, runSignal) {
2180
- if (runSignal === void 0) return unitSignal;
2181
- return (0, _orkestrel_abort.createAbort)({ signal: AbortSignal.any([unitSignal, runSignal]) }).signal;
2459
+ return AbortSignal.any([unitSignal, runSignal]);
2182
2460
  }
2183
- #fold(options, timeout) {
2184
- const signals = [];
2461
+ #fold(workflow, options, timeout) {
2462
+ const signals = [workflow.signal];
2185
2463
  if (options?.signal !== void 0) signals.push(options.signal);
2186
2464
  if (timeout !== void 0) signals.push(timeout.signal);
2187
2465
  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);
2466
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
2467
+ }
2468
+ #haltFrom(phases, index, workflow, runSignal) {
2469
+ if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2470
+ this.#skipFrom(phases, index);
2191
2471
  }
2192
2472
  #skipFrom(phases, index) {
2193
2473
  for (let cursor = index; cursor < phases.length; cursor += 1) {
@@ -2196,14 +2476,18 @@ var WorkflowRunner = class {
2196
2476
  for (const task of phase.tasks.tasks()) this.#skip(task);
2197
2477
  }
2198
2478
  }
2479
+ #skipCancelled(task, workflow, runSignal) {
2480
+ if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2481
+ this.#skip(task);
2482
+ }
2199
2483
  #skip(task) {
2200
2484
  if (task.status === "pending" || task.status === "running") task.skip();
2201
2485
  }
2202
2486
  #skipping(controller, runSignal) {
2203
- return controller.aborted || runSignal?.aborted === true;
2487
+ return controller.aborted || runSignal.aborted;
2204
2488
  }
2205
2489
  #cancelled(runSignal) {
2206
- return runSignal?.aborted === true;
2490
+ return runSignal.aborted;
2207
2491
  }
2208
2492
  #halted(workflow) {
2209
2493
  const status = workflow.status;
@@ -2216,11 +2500,8 @@ var WorkflowRunner = class {
2216
2500
  #completable(workflow) {
2217
2501
  return workflow.status === "pending";
2218
2502
  }
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);
2503
+ #isWorkflow(target) {
2504
+ return "destroyed" in target && "snapshot" in target && typeof target.snapshot === "function";
2224
2505
  }
2225
2506
  };
2226
2507
  //#endregion
@@ -2262,41 +2543,6 @@ function createWorkflowContract() {
2262
2543
  };
2263
2544
  }
2264
2545
  /**
2265
- * Compile the LENIENT workflow DRAFT contract — identical to
2266
- * {@link createWorkflowContract} EXCEPT `id` and `name` are OPTIONAL at all three levels
2267
- * (workflow / phase / task), so a small model can omit the six identity strings.
2268
- *
2269
- * @remarks
2270
- * The widened authoring surface {@link createWorkflowTool} parses an authored blob through
2271
- * before {@link import('./helpers.js').completeDraft} fills the missing ids/names. It does
2272
- * NOT relax the canonical contract — {@link createWorkflowContract} stays byte-for-byte
2273
- * unchanged and STRICT, and the completed draft is re-validated against THAT strict gate
2274
- * before running (soundness preserved). A PROVIDED `id` / `name` still carries `minLength: 1`,
2275
- * so an explicitly-empty `id: ''` is REJECTED (parses to `undefined`), never auto-filled —
2276
- * keeping "garbage" distinct from "omitted". `run` stays required.
2277
- *
2278
- * @returns The compiled {@link WorkflowDraft} contract
2279
- *
2280
- * @example
2281
- * ```ts
2282
- * import { createWorkflowDraftContract, completeDraft } from '@src/core'
2283
- *
2284
- * const draft = createWorkflowDraftContract()
2285
- * const parsed = draft.parse({ phases: [{ tasks: [{ run: { via: 'function', name: 'f' } }] }] })
2286
- * const definition = parsed && completeDraft(parsed) // ids/names filled positionally
2287
- * draft.parse({ id: '', phases: [] }) // undefined — an explicit empty id is rejected
2288
- * ```
2289
- */
2290
- function createWorkflowDraftContract() {
2291
- const contract = (0, _orkestrel_contract.createContract)(workflowDraftShape);
2292
- return {
2293
- schema: contract.schema,
2294
- is: contract.is,
2295
- generate: (random) => contract.generate(random),
2296
- parse: (value) => contract.parse(value)
2297
- };
2298
- }
2299
- /**
2300
2546
  * Build the live W-b entity tree from a {@link WorkflowDefinition} — the whole
2301
2547
  * {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
2302
2548
  * {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
@@ -2312,6 +2558,11 @@ function createWorkflowDraftContract() {
2312
2558
  * `options.phases[id].tasks[id]` (the AGENTS §8 nested-by-id bag). The W-b tree is the
2313
2559
  * state machine ONLY — it does not execute tasks (W-c drives the transitions).
2314
2560
  *
2561
+ * `options.functions` is the {@link import('./types.js').WorkflowFunctions} registry each live
2562
+ * task's `run` name resolves against ONCE at construction into its runtime
2563
+ * {@link import('./types.js').TaskInterface.handler} — a name omitted or absent from the
2564
+ * registry resolves to no handler (the no-handler rule).
2565
+ *
2315
2566
  * @param definition - The workflow definition to bring to life
2316
2567
  * @param options - Runtime options (initial listeners, `bail` override, per-node options)
2317
2568
  * @returns The live {@link WorkflowInterface} root
@@ -2371,10 +2622,13 @@ function restoreWorkflow(snapshot, options) {
2371
2622
  * untrusted JSON, so a status (or an override) outside
2372
2623
  * {@link import('./constants.js').WORKFLOW_STATUSES} /
2373
2624
  * {@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),
2625
+ * a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
2626
+ * present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
2627
+ * `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
2375
2628
  * 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.
2629
+ * The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
2630
+ * checked WHEN present. Structural shape beyond these fields is the contract's concern; this
2631
+ * guards exactly the fields the live state machine reads back.
2378
2632
  *
2379
2633
  * @param snapshot - The snapshot to validate
2380
2634
  */
@@ -2404,10 +2658,28 @@ function assertSnapshot(snapshot) {
2404
2658
  phase: phase.id,
2405
2659
  override: phase.override
2406
2660
  });
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
2661
+ if (phase.concurrency !== void 0 && (!Number.isInteger(phase.concurrency) || phase.concurrency < 1)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid concurrency`, {
2662
+ phase: phase.id,
2663
+ concurrency: phase.concurrency
2410
2664
  });
2665
+ for (const task of phase.tasks) {
2666
+ if (!TASK_STATUSES.includes(task.status)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid status`, {
2667
+ task: task.id,
2668
+ status: task.status
2669
+ });
2670
+ if (task.run !== void 0 && task.run.length < 1) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid run`, {
2671
+ task: task.id,
2672
+ run: task.run
2673
+ });
2674
+ if (task.retries !== void 0 && (!Number.isInteger(task.retries) || task.retries < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid retries`, {
2675
+ task: task.id,
2676
+ retries: task.retries
2677
+ });
2678
+ if (task.timeout !== void 0 && (!Number.isInteger(task.timeout) || task.timeout < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid timeout`, {
2679
+ task: task.id,
2680
+ timeout: task.timeout
2681
+ });
2682
+ }
2411
2683
  }
2412
2684
  }
2413
2685
  /**
@@ -2486,157 +2758,51 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
2486
2758
  /**
2487
2759
  * Create a workflow runner — a {@link WorkflowRunnerInterface} that EXECUTES a live W-b
2488
2760
  * workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent,
2489
- * each task dispatched BY NAME under the workflow's `bail` policy.
2761
+ * each task dispatched through its OWN resolved handler under the workflow's `bail` policy.
2490
2762
  *
2491
2763
  * @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
2764
+ * The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
2765
+ * carries no `functions` / `tools` / `agents` registry of its own: each live task already
2766
+ * resolved its own {@link import('./types.js').WorkflowFunction} into
2767
+ * {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
2768
+ * {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
2769
+ * Per-phase bounded concurrency is one {@link createRunner} per phase; `bail` maps onto that
2770
+ * Runner's fail-fast (`true` the first failure aborts the in-flight siblings + skips the
2771
+ * rest) vs settle-all (`false` failures are recorded, the run finishes); the run-level abort
2772
+ * / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
2773
+ * `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped scheduler.
2774
+ * `execute(definition, options?)` BUILDS the live tree from the definition itself (via
2775
+ * {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
2776
+ * the live entity (`start` → `complete` / `fail`), and resolves a
2501
2777
  * {@link import('./types.js').WorkflowResult}.
2502
2778
  *
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
2779
+ * Static tool / agent calling is OPT-IN: a caller wires a plain
2780
+ * {@link import('./types.js').WorkflowFunction} into its OWN {@link WorkflowOptions.functions}
2781
+ * registry, same as any other behavior the `@orkestrel/tool` package ships the
2782
+ * tool/agent adapter factories for that. A task with no resolved handler AUTO-COMPLETES
2507
2783
  * (the ROADMAP no-handler rule).
2508
2784
  *
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}.
2785
+ * @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
2786
+ * See {@link WorkflowRunnerOptions}.
2518
2787
  * @returns A working {@link WorkflowRunnerInterface}
2519
2788
  *
2520
2789
  * @example
2521
2790
  * ```ts
2522
- * import { createWorkflowRunner, createToolManager } from '@src/core'
2791
+ * import { createWorkflowRunner } from '@src/core'
2523
2792
  *
2524
- * const tools = createToolManager()
2525
- * const runner = createWorkflowRunner({
2526
- * functions: { compile: async (controller) => `built ${controller.task.id}` },
2527
- * tools,
2528
- * })
2793
+ * const runner = createWorkflowRunner()
2529
2794
  * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
2530
- * { id: 't', name: 'T', run: { via: 'function', name: 'compile' } },
2795
+ * { id: 't', name: 'T', run: 'compile' },
2531
2796
  * ] }] }
2532
- * const result = await runner.execute(definition) // builds + drives the tree
2797
+ * const result = await runner.execute(definition, {
2798
+ * functions: { compile: async (controller) => `built ${controller.task.id}` },
2799
+ * })
2533
2800
  * result.status // 'completed'
2534
2801
  * result.workflow.phase('p')?.task('t')?.status // 'completed'
2535
2802
  * ```
2536
2803
  */
2537
2804
  function createWorkflowRunner(options) {
2538
- return new WorkflowRunner(options?.functions ?? {}, options?.tools, options?.agents, options?.scheduler ?? createScheduler(), createWorkflowTool);
2539
- }
2540
- /**
2541
- * 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
2543
- * even a small model can author a complete tree, and its handler EXPANDS / COMPLETES the
2544
- * authored blob, validates it against the STRICT contract, runs it through `runner`, and
2545
- * returns the run SUMMARY (throwing a typed {@link WorkflowError} on failure).
2546
- *
2547
- * @remarks
2548
- * 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.
2554
- *
2555
- * **Widened authoring surface (additive — the canonical contract + runner stay STRICT and
2556
- * 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
2561
- * form, {@link import('./helpers.js').expandSteps}'d into one one-task phase per step);
2562
- * - a nested DRAFT with any `id`/`name` OMITTED — {@link createWorkflowDraftContract}-parsed then
2563
- * {@link import('./helpers.js').completeDraft}'d (missing ids synthesized positionally);
2564
- * - the full nested {@link WorkflowDefinition} — the advanced escape-hatch (documented in the
2565
- * description), accepted as the draft super-set.
2566
- *
2567
- * The handler conforms to the universal tool-handler contract (AGENTS §14): it returns the PLAIN
2568
- * run-summary VALUE on success and THROWS a typed {@link WorkflowError} on every failure path. It
2569
- * does NOT build a {@link ToolResult} itself — the `@orkestrel/agent` package's `ToolManager`
2570
- * performs the ONE canonical wrap (`{ id, name, value }` on a return; `{ id, name, error }` on a
2571
- * throw, ISOLATED so nothing escapes the run), so the outcome appears EXACTLY ONCE, identically,
2572
- * over BOTH the agent loop and MCP (a throw → MCP `isError: true`):
2573
- * - **No authored args** (an empty `arguments`) ⇒ runs the WRAPPED `definition`.
2574
- * - **A `steps` array** ⇒ the FLAT form: parse it, {@link import('./helpers.js').expandSteps} it.
2575
- * - **Otherwise** ⇒ the nested form: {@link createWorkflowDraftContract}-parse it,
2576
- * {@link import('./helpers.js').completeDraft} it.
2577
- * - **Strict gate** ⇒ the expanded / completed result is validated against
2578
- * {@link createWorkflowContract}.`is`; a blob that can't expand, or whose result fails the strict
2579
- * gate (e.g. an explicit empty `id`, `concurrency: 0`) ⇒ THROW a `TOOL` {@link WorkflowError} (no run).
2580
- * - **Over-deep / cyclic** ⇒ THROW a `DEPTH` {@link WorkflowError} when the nested run would exceed
2581
- * {@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}).
2585
- *
2586
- * @param definition - The workflow the tool runs when called with no authored args
2587
- * @param runner - The {@link WorkflowRunnerInterface} that executes the (nested) workflow
2588
- * @param options - The depth + ancestry to run the nested workflow under (see
2589
- * {@link WorkflowToolOptions}); omitted ⇒ depth `0` / empty ancestry (a top-level wrap)
2590
- * @returns A {@link ToolInterface} (named {@link import('./constants.js').WORKFLOW_TOOL_NAME})
2591
- * whose `parameters` advertise the FLAT authoring schema (the nested form stays accepted)
2592
- *
2593
- * @example
2594
- * ```ts
2595
- * import { createWorkflowRunner, createWorkflowTool, createToolManager } from '@src/core'
2596
- *
2597
- * const runner = createWorkflowRunner()
2598
- * const tool = createWorkflowTool(definition, runner)
2599
- * const tools = createToolManager()
2600
- * tools.add(tool) // a model can now author + run a workflow in one call
2601
- * ```
2602
- */
2603
- function createWorkflowTool(definition, runner, options) {
2604
- const strict = createWorkflowContract();
2605
- const draft = createWorkflowDraftContract();
2606
- const steps = (0, _orkestrel_contract.createContract)(workflowStepsShape);
2607
- const depth = options?.depth ?? 0;
2608
- const ancestry = options?.ancestry ?? [];
2609
- return (0, _orkestrel_agent.createTool)({
2610
- name: WORKFLOW_TOOL_NAME,
2611
- description: WORKFLOW_TOOL_DESCRIPTION,
2612
- parameters: (0, _orkestrel_contract.schemaToParameters)(steps.schema),
2613
- execute: async (args) => {
2614
- let target;
2615
- if (Object.keys(args).length === 0) target = definition;
2616
- else if (Array.isArray(args.steps)) {
2617
- const flat = steps.parse(args);
2618
- target = flat === void 0 ? void 0 : expandSteps(flat);
2619
- } else {
2620
- const parsed = draft.parse(args);
2621
- target = parsed === void 0 ? void 0 : completeDraft(parsed);
2622
- }
2623
- if (target === void 0 || !strict.is(target)) throw new WorkflowError("TOOL", "malformed workflow definition", { workflow: definition.id });
2624
- if (depth + 1 > 8) throw new WorkflowError("DEPTH", `nested workflow exceeds max depth 8`, {
2625
- workflow: target.id,
2626
- depth,
2627
- max: 8
2628
- });
2629
- const tag = workflowTag(target.id);
2630
- if (ancestry.includes(tag)) throw new WorkflowError("DEPTH", `workflow '${target.id}' is already an ancestor (cycle)`, {
2631
- workflow: target.id,
2632
- ancestry: [...ancestry]
2633
- });
2634
- return workflowToolSummary(await runner.execute(target, {
2635
- depth: depth + 1,
2636
- ancestry: [...ancestry, tag]
2637
- }));
2638
- }
2639
- });
2805
+ return new WorkflowRunner(options?.scheduler ?? createScheduler());
2640
2806
  }
2641
2807
  /**
2642
2808
  * Create the safe cross-environment cooperative-yield default — a
@@ -2734,7 +2900,6 @@ exports.Controller = Controller;
2734
2900
  exports.DEFAULT_BAIL = DEFAULT_BAIL;
2735
2901
  exports.DEFAULT_PHASE_CONCURRENCY = DEFAULT_PHASE_CONCURRENCY;
2736
2902
  exports.DatabaseWorkflowStore = DatabaseWorkflowStore;
2737
- exports.MAX_WORKFLOW_DEPTH = MAX_WORKFLOW_DEPTH;
2738
2903
  exports.MemoryWorkflowStore = MemoryWorkflowStore;
2739
2904
  exports.PHASE_STATUSES = PHASE_STATUSES;
2740
2905
  exports.Phase = Phase;
@@ -2743,29 +2908,20 @@ exports.Runner = Runner;
2743
2908
  exports.Scheduler = Scheduler;
2744
2909
  exports.TASK_STATUSES = TASK_STATUSES;
2745
2910
  exports.TASK_TRANSITIONS = TASK_TRANSITIONS;
2746
- exports.TASK_VIAS = TASK_VIAS;
2747
2911
  exports.TERMINAL_TASK_STATUSES = TERMINAL_TASK_STATUSES;
2748
2912
  exports.Task = Task;
2749
2913
  exports.TaskController = TaskController;
2750
2914
  exports.TaskManager = TaskManager;
2751
2915
  exports.WORKFLOW_STATUSES = WORKFLOW_STATUSES;
2752
- exports.WORKFLOW_TOOL_DESCRIPTION = WORKFLOW_TOOL_DESCRIPTION;
2753
- exports.WORKFLOW_TOOL_FLAT_EXAMPLE = WORKFLOW_TOOL_FLAT_EXAMPLE;
2754
- exports.WORKFLOW_TOOL_NAME = WORKFLOW_TOOL_NAME;
2755
- exports.WORKFLOW_TOOL_NESTED_EXAMPLE = WORKFLOW_TOOL_NESTED_EXAMPLE;
2756
2916
  exports.Workflow = Workflow;
2757
2917
  exports.WorkflowError = WorkflowError;
2758
2918
  exports.WorkflowRunner = WorkflowRunner;
2759
- exports.agentTag = agentTag;
2760
2919
  exports.assertSnapshot = assertSnapshot;
2761
2920
  exports.buildPhaseContext = buildPhaseContext;
2762
2921
  exports.buildTaskContext = buildTaskContext;
2763
2922
  exports.buildWorkflowContext = buildWorkflowContext;
2764
2923
  exports.canTransitionTask = canTransitionTask;
2765
2924
  exports.collectResults = collectResults;
2766
- exports.completeDraft = completeDraft;
2767
- exports.completePhaseDraft = completePhaseDraft;
2768
- exports.completeTaskDraft = completeTaskDraft;
2769
2925
  exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
2770
2926
  exports.createDeferred = createDeferred;
2771
2927
  exports.createMemoryWorkflowStore = createMemoryWorkflowStore;
@@ -2773,33 +2929,27 @@ exports.createRunner = createRunner;
2773
2929
  exports.createScheduler = createScheduler;
2774
2930
  exports.createWorkflow = createWorkflow;
2775
2931
  exports.createWorkflowContract = createWorkflowContract;
2776
- exports.createWorkflowDraftContract = createWorkflowDraftContract;
2777
2932
  exports.createWorkflowRunner = createWorkflowRunner;
2778
- exports.createWorkflowTool = createWorkflowTool;
2779
2933
  exports.definitionToSnapshot = definitionToSnapshot;
2934
+ exports.deriveBoundary = deriveBoundary;
2780
2935
  exports.derivePhaseStatus = derivePhaseStatus;
2781
2936
  exports.deriveWorkflowStatus = deriveWorkflowStatus;
2782
- exports.expandSteps = expandSteps;
2783
- exports.isAgentTask = isAgentTask;
2784
- exports.isFunctionTask = isFunctionTask;
2937
+ exports.failure = failure;
2938
+ exports.findFailure = findFailure;
2939
+ exports.insertEntry = insertEntry;
2785
2940
  exports.isTerminalStatus = isTerminalStatus;
2786
- exports.isToolTask = isToolTask;
2787
2941
  exports.isWorkflowError = isWorkflowError;
2788
2942
  exports.isWorkflowSnapshot = isWorkflowSnapshot;
2943
+ exports.moveEntry = moveEntry;
2944
+ exports.parkSignal = parkSignal;
2789
2945
  exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
2790
- exports.phaseDraftShape = phaseDraftShape;
2791
2946
  exports.phaseShape = phaseShape;
2947
+ exports.phaseUpdateShape = phaseUpdateShape;
2792
2948
  exports.restoreWorkflow = restoreWorkflow;
2793
- exports.stepShape = stepShape;
2794
- exports.stepToForm = stepToForm;
2949
+ exports.success = success;
2795
2950
  exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
2796
- exports.taskDraftShape = taskDraftShape;
2797
- exports.taskFormShape = taskFormShape;
2798
2951
  exports.taskShape = taskShape;
2799
- exports.workflowDraftShape = workflowDraftShape;
2952
+ exports.taskUpdateShape = taskUpdateShape;
2800
2953
  exports.workflowShape = workflowShape;
2801
- exports.workflowStepsShape = workflowStepsShape;
2802
- exports.workflowTag = workflowTag;
2803
- exports.workflowToolSummary = workflowToolSummary;
2804
2954
 
2805
2955
  //# sourceMappingURL=index.cjs.map