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