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