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