octaflow 0.26.0 → 0.28.0
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 +6 -2
- package/dist/ai/index.d.ts +27 -2
- package/dist/ai/index.js +51 -3
- package/dist/{core-DNWPwFaE.js → core-C9h3-KFr.js} +217 -1
- package/dist/{dispatcher-g7Ct0J6w.d.ts → dispatcher-B4haXU4w.d.ts} +1 -1
- package/dist/{hooks-BdxIcc92.d.ts → hooks-BV-Vany9.d.ts} +1 -1
- package/dist/{index-BURAYZui.d.ts → index-B8SvtiYn.d.ts} +185 -2
- package/dist/index.d.ts +11 -5
- package/dist/index.js +88 -2
- package/dist/{observability-3wVqt3tZ.d.ts → observability-DRUJOpiQ.d.ts} +33 -3
- package/dist/pg-boss/index.d.ts +2 -2
- package/dist/postgres/index.d.ts +2 -2
- package/dist/postgres/index.js +31 -4
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -200,8 +200,10 @@ next to what your steps actually do.
|
|
|
200
200
|
| 🔁 | **Retry & timeout** — per-step `maxAttempts`, fixed/exponential backoff, wall-clock timeout |
|
|
201
201
|
| 💤 | **Durable sleep** — hold a step in the queue for N ms (survives restarts) |
|
|
202
202
|
| 🔀 | **Conditional branching** — `when` guards skip a step and its branch; `join: 'any'` converges |
|
|
203
|
+
| 🎯 | **Decisions** — typed choices with a confidence from a swappable decider (a function, a model, your own); below a threshold a person settles it |
|
|
203
204
|
| ⏳ | **Deadlines** — a budget on a suspended step (fail, or continue with a stand-in answer) and on a whole run |
|
|
204
205
|
| ♻️ | **Retry a failed run** — `retryWorkflow` resumes from the failure point; completed steps keep their output |
|
|
206
|
+
| 🧹 | **Delete the runs about a record** — `purgeWorkflows({ entityRef })` cancels and deletes them with their sub-workflow runs and steps, for an erasure or a deleted record; refuses only while a step is executing |
|
|
205
207
|
| 🚦 | **Concurrency & rate limiting** — per-step-type caps and token buckets via a pluggable gate |
|
|
206
208
|
| ⏰ | **Cron / scheduled starts** — fire workflows on a schedule (pg-boss) |
|
|
207
209
|
| 🔑 | **Start idempotency** — a dedup key collapses double-clicks / overlapping ticks |
|
|
@@ -321,11 +323,12 @@ Full docs — concepts, every feature, production wiring and the API reference
|
|
|
321
323
|
| [Retry & timeout](https://octaflow.octabits.io/core/retry-and-timeout/) | attempt budgets, backoff, and how a failure is classified |
|
|
322
324
|
| [Fan-out & map](https://octaflow.octabits.io/core/fan-out-and-map/) | one child step per item of a runtime list |
|
|
323
325
|
| [Branching](https://octaflow.octabits.io/core/branching/) | `when` guards and join rules — if/else over a static DAG |
|
|
326
|
+
| [Decisions](https://octaflow.octabits.io/core/decisions/) | typed choices with a confidence, a swappable decider, a person below the threshold |
|
|
324
327
|
| [Signals](https://octaflow.octabits.io/core/signals/) · [Sub-workflows](https://octaflow.octabits.io/core/sub-workflows/) · [Saga](https://octaflow.octabits.io/core/saga-compensation/) | suspend, nest, and roll back |
|
|
325
328
|
| [Deadlines](https://octaflow.octabits.io/core/deadlines/) | budgets for a suspended step and for a whole run |
|
|
326
329
|
| [Heartbeats](https://octaflow.octabits.io/core/heartbeats/) | liveness for long steps, and interrupting one that was cancelled |
|
|
327
330
|
| [Postgres & pg-boss](https://octaflow.octabits.io/running/postgres-and-pg-boss/) | production wiring, workers, DLQ, cron |
|
|
328
|
-
| [Cancellation & recovery](https://octaflow.octabits.io/running/cancellation-and-recovery/) | cancelling a run, sweeping steps a crash left behind,
|
|
331
|
+
| [Cancellation & recovery](https://octaflow.octabits.io/running/cancellation-and-recovery/) | cancelling a run, sweeping steps a crash left behind, retrying a failed run, and deleting the runs about a record |
|
|
329
332
|
| [Observability](https://octaflow.octabits.io/running/observability/) · [Live progress](https://octaflow.octabits.io/running/live-progress/) | lifecycle events, spans, and streaming them to a browser |
|
|
330
333
|
| [The AI add-on](https://octaflow.octabits.io/extending/ai/) | token/cost capture, quota, usage rollups |
|
|
331
334
|
| [Extending](https://octaflow.octabits.io/extending/interfaces/) | custom stores, dispatchers and gates |
|
|
@@ -355,8 +358,9 @@ Runnable, focused examples live in [`examples/`](./examples) — see [`examples/
|
|
|
355
358
|
| 14 | `14-live-progress.ts` | `FlowObserver` → SSE fan-out (build your own dashboard) |
|
|
356
359
|
| 15 | `15-conditional-branching.ts` | `when` guards + a `join: 'any'` convergence |
|
|
357
360
|
| 16 | `16-deadlines-and-retry.ts` | wait deadlines, run deadlines, `retryWorkflow`, heartbeats |
|
|
361
|
+
| 17 | `17-decisions.ts` | a decision step, a review below the threshold, branches on the outcome |
|
|
358
362
|
|
|
359
|
-
The in-memory examples (01–11, 14–
|
|
363
|
+
The in-memory examples (01–11, 14–17) share a small driver, [`examples/runtime.ts`](./examples/runtime.ts),
|
|
360
364
|
that builds an engine over the in-memory store and an in-process queue you drain.
|
|
361
365
|
|
|
362
366
|
---
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as TypedStep, H as RetryPolicy, M as TypedWorkflow, f as Decider, j as TypedStepContext, vt as FlowErrorShape, yt as Result } from "../index-B8SvtiYn.js";
|
|
2
2
|
import { n as Logger } from "../logger-BCus3AOa.js";
|
|
3
|
-
import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-
|
|
3
|
+
import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-BV-Vany9.js";
|
|
4
4
|
import { C as isSubprocessorResidency, S as embeddingDimensionsFor, _ as EmbeddingProviderMeta, a as AiProcessingRegion, b as OpenAiCompatibleProviderId, c as AiProviderModelOption, d as AiVendorResidency, f as DEFAULT_AI_PROVIDER, g as EmbeddingProviderId, h as EMBEDDING_PROVIDER_IDS, i as AiEndpointResidency, l as AiResidencyError, m as EMBEDDING_PROVIDERS, n as AI_PROVIDER_IDS, o as AiProviderId, p as DEFAULT_EMBEDDING_PROVIDER, r as AiEndpointRegion, s as AiProviderMeta, t as AI_PROVIDERS, u as AiResidencyResult, v as OPENAI_COMPATIBLE_PROVIDER, w as resolveEndpointResidency, x as OpenAiCompatibleProviderMeta, y as OPENAI_COMPATIBLE_PROVIDER_ID } from "../provider-registry-BvXBqJDR.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { EmbeddingModelV4, LanguageModelV4, SharedV4ProviderMetadata } from "@ai-sdk/provider";
|
|
@@ -148,6 +148,31 @@ export declare function buildAiWorkflow<TInput extends Record<string, unknown>,
|
|
|
148
148
|
steps: TSteps;
|
|
149
149
|
}): TypedWorkflow<TInput, AiContext<THost>>;
|
|
150
150
|
//#endregion
|
|
151
|
+
//#region src/ai/model-decider.d.ts
|
|
152
|
+
export interface ModelDeciderOptions {
|
|
153
|
+
/**
|
|
154
|
+
* The model to ask. Omitted: the step's `ctx.context.model`, which the AI
|
|
155
|
+
* hooks instrument for token usage and cost. Pass a model wrapped by your
|
|
156
|
+
* disclosure gate either way — the decision input leaves the process.
|
|
157
|
+
*/
|
|
158
|
+
model?: LanguageModelV4;
|
|
159
|
+
/** Recorded on every decision. Default `model:<modelId>` of the model that ran. */
|
|
160
|
+
id?: string;
|
|
161
|
+
/** Prepended to the request's own instructions. */
|
|
162
|
+
system?: string;
|
|
163
|
+
/** Passed to the AI SDK. Default 0 — the step's retry policy owns retries. */
|
|
164
|
+
maxRetries?: number;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A decider over a language model with structured output: the options become
|
|
168
|
+
* an enum the model must choose from, and the model states its confidence.
|
|
169
|
+
*
|
|
170
|
+
* `calibrated` is `false` — a language model's stated confidence is not a
|
|
171
|
+
* probability you can set a threshold against with any precision. Treat its
|
|
172
|
+
* threshold as a policy knob and check it against what reviewers overrule.
|
|
173
|
+
*/
|
|
174
|
+
export declare function createModelDecider<THost = unknown>(options?: ModelDeciderOptions): Decider<AiContext<THost>>;
|
|
175
|
+
//#endregion
|
|
151
176
|
//#region src/ai/hooks.d.ts
|
|
152
177
|
export interface AiModelResolver<THost = unknown> {
|
|
153
178
|
/** Resolve the base (uninstrumented) model for a step. */
|
package/dist/ai/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as ok, E as err, T as noopLogger, _ as defineStep, m as buildWorkflow } from "../core-C9h3-KFr.js";
|
|
2
2
|
import { a as readRefusalDetails, i as isAiRefusalError, n as REFUSAL_FINISH_REASON, r as createRefusalAwareModel, t as AiRefusalError } from "../refusal-BsL2HPDb.js";
|
|
3
3
|
import { a as EMBEDDING_PROVIDERS, c as OPENAI_COMPATIBLE_PROVIDER_ID, d as resolveEndpointResidency, i as DEFAULT_EMBEDDING_PROVIDER, l as embeddingDimensionsFor, n as AI_PROVIDER_IDS, o as EMBEDDING_PROVIDER_IDS, r as DEFAULT_AI_PROVIDER, s as OPENAI_COMPATIBLE_PROVIDER, t as AI_PROVIDERS, u as isSubprocessorResidency } from "../provider-registry-BRwjC6_3.js";
|
|
4
|
-
import {
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { Output, generateText, wrapEmbeddingModel, wrapLanguageModel } from "ai";
|
|
5
6
|
//#region src/ai/instrumented-model.ts
|
|
6
7
|
function createUsageAccumulator() {
|
|
7
8
|
const total = {
|
|
@@ -318,6 +319,53 @@ function buildAiWorkflow(config) {
|
|
|
318
319
|
return buildWorkflow(config);
|
|
319
320
|
}
|
|
320
321
|
//#endregion
|
|
322
|
+
//#region src/ai/model-decider.ts
|
|
323
|
+
const DEFAULT_SYSTEM = [
|
|
324
|
+
"You classify the input by answering each question with exactly one of its options.",
|
|
325
|
+
"For each answer give a confidence between 0 and 1: the probability that the option is right.",
|
|
326
|
+
"Use a low confidence when the input is ambiguous or the options do not fit it well. Keep reasons to one sentence."
|
|
327
|
+
].join(" ");
|
|
328
|
+
function responseSchema(questions) {
|
|
329
|
+
return z.object(Object.fromEntries(Object.entries(questions).map(([key, options]) => [key, z.object({
|
|
330
|
+
choice: z.enum(options),
|
|
331
|
+
confidence: z.number().min(0).max(1),
|
|
332
|
+
reason: z.string()
|
|
333
|
+
})])));
|
|
334
|
+
}
|
|
335
|
+
function prompt(request) {
|
|
336
|
+
return `Questions:\n${Object.entries(request.questions).map(([key, options]) => `- ${key}: ${options.join(" | ")}`).join("\n")}\n\nInput:\n${JSON.stringify(request.input, null, 2)}`;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A decider over a language model with structured output: the options become
|
|
340
|
+
* an enum the model must choose from, and the model states its confidence.
|
|
341
|
+
*
|
|
342
|
+
* `calibrated` is `false` — a language model's stated confidence is not a
|
|
343
|
+
* probability you can set a threshold against with any precision. Treat its
|
|
344
|
+
* threshold as a policy knob and check it against what reviewers overrule.
|
|
345
|
+
*/
|
|
346
|
+
function createModelDecider(options = {}) {
|
|
347
|
+
return {
|
|
348
|
+
id: options.id ?? (options.model ? `model:${options.model.modelId}` : "model"),
|
|
349
|
+
calibrated: false,
|
|
350
|
+
async decide(request, context) {
|
|
351
|
+
const model = options.model ?? context.model;
|
|
352
|
+
const system = [options.system ?? DEFAULT_SYSTEM, request.instructions].filter(Boolean).join("\n\n");
|
|
353
|
+
const { output } = await generateText({
|
|
354
|
+
model,
|
|
355
|
+
system,
|
|
356
|
+
prompt: prompt(request),
|
|
357
|
+
output: Output.object({ schema: responseSchema(request.questions) }),
|
|
358
|
+
maxRetries: options.maxRetries ?? 0,
|
|
359
|
+
abortSignal: request.signal
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
answers: output,
|
|
363
|
+
decider: options.id ?? `model:${model.modelId}`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
321
369
|
//#region src/ai/hooks.ts
|
|
322
370
|
/**
|
|
323
371
|
* Wire the AI behaviors (model instrumentation, token/cost capture, quota,
|
|
@@ -662,4 +710,4 @@ function thinkingSettingsFor(providerId, modelId, opts) {
|
|
|
662
710
|
return { temperature: opts.temperature };
|
|
663
711
|
}
|
|
664
712
|
//#endregion
|
|
665
|
-
export { AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_PRICING, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createRefusalAwareModel, createUsageAccumulator, defineAiStep, embeddingDimensionsFor, estimateCostMicros, isAiRefusalError, isSubprocessorResidency, readRefusalDetails, resolveEndpointResidency, supportsAdaptiveThinking, supportsToolSearch, thinkingSettingsFor };
|
|
713
|
+
export { AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_PRICING, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createRefusalAwareModel, createUsageAccumulator, defineAiStep, embeddingDimensionsFor, estimateCostMicros, isAiRefusalError, isSubprocessorResidency, readRefusalDetails, resolveEndpointResidency, supportsAdaptiveThinking, supportsToolSearch, thinkingSettingsFor };
|
|
@@ -515,6 +515,222 @@ function buildWorkflow(config) {
|
|
|
515
515
|
};
|
|
516
516
|
}
|
|
517
517
|
//#endregion
|
|
518
|
+
//#region src/core/decision.ts
|
|
519
|
+
/**
|
|
520
|
+
* A decider over a plain function: every answer has confidence 1, so it never
|
|
521
|
+
* asks for review. For rules you can state, and for tests.
|
|
522
|
+
*/
|
|
523
|
+
function functionDecider(id, decide) {
|
|
524
|
+
return {
|
|
525
|
+
id: `function:${id}`,
|
|
526
|
+
calibrated: true,
|
|
527
|
+
async decide(request, context) {
|
|
528
|
+
const choices = await decide(request, context);
|
|
529
|
+
return { answers: Object.fromEntries(Object.entries(choices).map(([key, choice]) => [key, {
|
|
530
|
+
choice,
|
|
531
|
+
confidence: 1
|
|
532
|
+
}])) };
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function answerSchema(options) {
|
|
537
|
+
const option = z.enum(options);
|
|
538
|
+
return z.object({
|
|
539
|
+
choice: option,
|
|
540
|
+
confidence: z.number().min(0).max(1),
|
|
541
|
+
distribution: z.partialRecord(option, z.number().min(0).max(1)).optional(),
|
|
542
|
+
reason: z.string().optional()
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function decisionOutputSchema(questions) {
|
|
546
|
+
const keys = Object.keys(questions);
|
|
547
|
+
return z.object({
|
|
548
|
+
answers: z.object(Object.fromEntries(Object.entries(questions).map(([key, options]) => [key, answerSchema(options)]))),
|
|
549
|
+
decider: z.string(),
|
|
550
|
+
calibrated: z.boolean(),
|
|
551
|
+
review: z.array(z.enum(keys))
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function decisionReviewSchema(questions) {
|
|
555
|
+
return z.object({
|
|
556
|
+
answers: z.object(Object.fromEntries(Object.entries(questions).map(([key, options]) => [key, z.enum(options).optional()]))).strict(),
|
|
557
|
+
reviewer: z.string().optional()
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function decisionOutcomeSchema(questions) {
|
|
561
|
+
const entries = Object.entries(questions);
|
|
562
|
+
return z.object({
|
|
563
|
+
choices: z.object(Object.fromEntries(entries.map(([key, options]) => [key, z.enum(options)]))),
|
|
564
|
+
decidedBy: z.object(Object.fromEntries(entries.map(([key]) => [key, z.enum(["decider", "person"])]))),
|
|
565
|
+
decider: z.string(),
|
|
566
|
+
reviewer: z.string().optional()
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const emptyInputSchema = z.object({});
|
|
570
|
+
/**
|
|
571
|
+
* Hold a decider's response to the questions: every question answered, every
|
|
572
|
+
* choice one of its options. A decider that breaks this is broken, not flaky —
|
|
573
|
+
* the error is not retried.
|
|
574
|
+
*/
|
|
575
|
+
function checkResponse(type, questions, decider, response) {
|
|
576
|
+
const answers = {};
|
|
577
|
+
for (const [key, options] of Object.entries(questions)) {
|
|
578
|
+
const answer = response.answers[key];
|
|
579
|
+
if (!answer) throw nonRetryableError(`[${type}] decider '${decider}' gave no answer to '${key}'`);
|
|
580
|
+
if (!options.includes(answer.choice)) throw nonRetryableError(`[${type}] decider '${decider}' answered '${key}' with '${answer.choice}', not one of: ${options.join(", ")}`);
|
|
581
|
+
if (!(answer.confidence >= 0 && answer.confidence <= 1)) throw nonRetryableError(`[${type}] decider '${decider}' gave '${key}' a confidence of ${answer.confidence}, outside [0, 1]`);
|
|
582
|
+
answers[key] = answer;
|
|
583
|
+
}
|
|
584
|
+
return answers;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Define a **decision** step: the decider answers each question with one of
|
|
588
|
+
* its options and a confidence; the output is typed by the options and
|
|
589
|
+
* validated against them, so a dependent can branch on it with a plain `when`.
|
|
590
|
+
*
|
|
591
|
+
* ```ts
|
|
592
|
+
* const triage = defineDecisionStep({
|
|
593
|
+
* type: 'mail:triage',
|
|
594
|
+
* workflowInputSchema: input,
|
|
595
|
+
* dependencies: { parseMail },
|
|
596
|
+
* questions: {
|
|
597
|
+
* tone: ['aggressive', 'friendly'],
|
|
598
|
+
* audience: ['invoicing', 'customer-support', 'tech-support'],
|
|
599
|
+
* },
|
|
600
|
+
* input: (ctx) => ctx.deps.parseMail,
|
|
601
|
+
* decider: createModelDecider(),
|
|
602
|
+
* threshold: 0.8,
|
|
603
|
+
* });
|
|
604
|
+
* ```
|
|
605
|
+
*
|
|
606
|
+
* Questions under `threshold` land in `output.review`; pair the step with
|
|
607
|
+
* {@link defineDecisionReviewStep} and {@link defineDecisionOutcomeStep} to have
|
|
608
|
+
* a person settle them.
|
|
609
|
+
*/
|
|
610
|
+
function defineDecisionStep(config) {
|
|
611
|
+
const { type, questions, decider, threshold } = config;
|
|
612
|
+
const keys = Object.keys(questions);
|
|
613
|
+
if (keys.length === 0) throw new Error(`[${type}] a decision needs at least one question`);
|
|
614
|
+
for (const [key, options] of Object.entries(questions)) if (new Set(options).size !== options.length) throw new Error(`[${type}] question '${key}' repeats an option`);
|
|
615
|
+
const thresholdOf = (key) => typeof threshold === "number" ? threshold : threshold?.[key];
|
|
616
|
+
return {
|
|
617
|
+
...defineStep({
|
|
618
|
+
type,
|
|
619
|
+
workflowInputSchema: config.workflowInputSchema,
|
|
620
|
+
outputSchema: decisionOutputSchema(questions),
|
|
621
|
+
dependencies: config.dependencies,
|
|
622
|
+
when: config.when,
|
|
623
|
+
join: config.join,
|
|
624
|
+
retry: config.retry,
|
|
625
|
+
isRetryable: config.isRetryable,
|
|
626
|
+
timeoutMs: config.timeoutMs,
|
|
627
|
+
handler: async (ctx) => {
|
|
628
|
+
const response = await decider.decide({
|
|
629
|
+
questions,
|
|
630
|
+
input: await config.input(ctx),
|
|
631
|
+
instructions: config.instructions,
|
|
632
|
+
signal: ctx.signal
|
|
633
|
+
}, ctx.context);
|
|
634
|
+
const deciderId = response.decider ?? decider.id;
|
|
635
|
+
const answers = checkResponse(type, questions, deciderId, response);
|
|
636
|
+
const review = keys.filter((key) => {
|
|
637
|
+
const t = thresholdOf(key);
|
|
638
|
+
return t !== void 0 && answers[key].confidence < t;
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
answers,
|
|
642
|
+
decider: deciderId,
|
|
643
|
+
calibrated: decider.calibrated,
|
|
644
|
+
review
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}),
|
|
648
|
+
questions
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/** The single entry of a one-step dependency map, or a definition-time error. */
|
|
652
|
+
function soleEntry(type, role, map) {
|
|
653
|
+
const entries = Object.entries(map);
|
|
654
|
+
if (entries.length !== 1) throw new Error(`[${type}] '${role}' takes exactly one step, keyed by its step key — got ${entries.length}`);
|
|
655
|
+
return entries[0];
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Define the **review** of a decision: a wait step that suspends only when the
|
|
659
|
+
* decision flagged a question, until `engine.resumeStep` delivers a
|
|
660
|
+
* {@link DecisionReview} — the options a person chose. When nothing was
|
|
661
|
+
* flagged the step is skipped.
|
|
662
|
+
*
|
|
663
|
+
* `onTimeout: 'accept'` settles an expired review by keeping the decider's
|
|
664
|
+
* answers; the default `'fail'` ends the run.
|
|
665
|
+
*/
|
|
666
|
+
function defineDecisionReviewStep(config) {
|
|
667
|
+
const [key, decision] = soleEntry(config.type, "decision", config.decision);
|
|
668
|
+
return defineStep({
|
|
669
|
+
type: config.type,
|
|
670
|
+
workflowInputSchema: emptyInputSchema,
|
|
671
|
+
outputSchema: decisionReviewSchema(decision.questions),
|
|
672
|
+
dependencies: config.decision,
|
|
673
|
+
waitForEvent: true,
|
|
674
|
+
timeoutMs: config.timeoutMs,
|
|
675
|
+
onTimeout: config.onTimeout === "accept" ? { output: { answers: {} } } : "fail",
|
|
676
|
+
when: (ctx) => ctx.deps[key].review.length > 0,
|
|
677
|
+
handler: async () => ({ answers: {} })
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Merge a decision with its review: a person's choice wins where they made
|
|
682
|
+
* one, the decider's stands everywhere else. Also what
|
|
683
|
+
* {@link defineDecisionOutcomeStep} runs.
|
|
684
|
+
*/
|
|
685
|
+
function resolveDecision(decision, review) {
|
|
686
|
+
const choices = {};
|
|
687
|
+
const decidedBy = {};
|
|
688
|
+
for (const [key, answer] of Object.entries(decision.answers)) {
|
|
689
|
+
const chosen = (review?.answers)?.[key];
|
|
690
|
+
choices[key] = chosen ?? answer.choice;
|
|
691
|
+
decidedBy[key] = chosen === void 0 ? "decider" : "person";
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
choices,
|
|
695
|
+
decidedBy,
|
|
696
|
+
decider: decision.decider,
|
|
697
|
+
...review?.reviewer !== void 0 ? { reviewer: review.reviewer } : {}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Define the **outcome** of a decision: one step that waits for the review
|
|
702
|
+
* when there is one, runs straight after the decision when there is not, and
|
|
703
|
+
* outputs the settled {@link DecisionOutcome}. Branch on it downstream:
|
|
704
|
+
*
|
|
705
|
+
* ```ts
|
|
706
|
+
* const invoicing = defineStep({
|
|
707
|
+
* dependencies: { outcome },
|
|
708
|
+
* when: (ctx) => ctx.deps.outcome.choices.audience === 'invoicing',
|
|
709
|
+
* // …
|
|
710
|
+
* });
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
function defineDecisionOutcomeStep(config) {
|
|
714
|
+
const [decisionKey, decision] = soleEntry(config.type, "decision", config.decision);
|
|
715
|
+
const [reviewKey] = soleEntry(config.type, "review", config.review);
|
|
716
|
+
return defineStep({
|
|
717
|
+
type: config.type,
|
|
718
|
+
workflowInputSchema: emptyInputSchema,
|
|
719
|
+
outputSchema: decisionOutcomeSchema(decision.questions),
|
|
720
|
+
dependencies: {
|
|
721
|
+
...config.decision,
|
|
722
|
+
...config.review
|
|
723
|
+
},
|
|
724
|
+
join: "any",
|
|
725
|
+
handler: async (ctx) => {
|
|
726
|
+
const deps = ctx.deps;
|
|
727
|
+
const decided = deps[decisionKey];
|
|
728
|
+
if (!decided) throw nonRetryableError(`[${config.type}] decision '${decisionKey}' has no output`);
|
|
729
|
+
return resolveDecision(decided, deps[reviewKey]);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
//#endregion
|
|
518
734
|
//#region src/core/public-view.ts
|
|
519
735
|
/**
|
|
520
736
|
* Public wire view of a workflow — the projection every HTTP consumer was
|
|
@@ -653,4 +869,4 @@ function toPublicWorkflow(workflow) {
|
|
|
653
869
|
};
|
|
654
870
|
}
|
|
655
871
|
//#endregion
|
|
656
|
-
export { ok as
|
|
872
|
+
export { nonRetryableError as C, ok as D, err as E, markRetryable as S, noopLogger as T, defineStep as _, WORKFLOW_STATUS_SCHEMA as a, explicitRetryability as b, toPublicWorkflow as c, defineDecisionStep as d, functionDecider as f, defineSleepStep as g, defineMapStep as h, STEP_DISPLAY_STATUS_SCHEMA as i, defineDecisionOutcomeStep as l, buildWorkflow as m, PUBLIC_WORKFLOW_STEP_SCHEMA as n, toDisplayStepStatus as o, resolveDecision as p, STEP_DISPLAY_STATUS as r, toPublicStep as s, PUBLIC_WORKFLOW_SCHEMA as t, defineDecisionReviewStep as u, defineSubWorkflowStep as v, retryableError as w, isRetryableError as x, defineWaitStep as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as StepId, ot as WorkflowId, vt as FlowErrorShape, yt as Result } from "./index-B8SvtiYn.js";
|
|
2
2
|
//#region src/core/dispatcher.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* What a step job is for. A queue carries two kinds of work for a step, and
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as StepId, U as StartOptions, at as WorkflowDefinition, et as StepRecord, lt as WorkflowRecord, ot as WorkflowId, vt as FlowErrorShape, yt as Result } from "./index-B8SvtiYn.js";
|
|
2
2
|
//#region src/core/hooks.d.ts
|
|
3
3
|
interface BeforeStartArgs {
|
|
4
4
|
definition: WorkflowDefinition;
|
|
@@ -373,12 +373,25 @@ interface WorkflowNotRetryableError extends FlowErrorShape {
|
|
|
373
373
|
key: 'workflow_not_retryable';
|
|
374
374
|
status: WorkflowStatus;
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* `purgeWorkflows` refused: a step of one of the runs is executing right now (a worker
|
|
378
|
+
* holds it, or a compensation handler is running). Nothing was cancelled or deleted.
|
|
379
|
+
*/
|
|
380
|
+
interface WorkflowStepRunningError extends FlowErrorShape {
|
|
381
|
+
key: 'workflow_step_running';
|
|
382
|
+
/** The steps that are executing, by run. */
|
|
383
|
+
running: Array<{
|
|
384
|
+
workflowId: WorkflowId;
|
|
385
|
+
stepKey: string;
|
|
386
|
+
status: StepStatus;
|
|
387
|
+
}>;
|
|
388
|
+
}
|
|
376
389
|
/**
|
|
377
390
|
* Engine error type. The named members cover flow-core's own failures; the
|
|
378
391
|
* bare `FlowErrorShape` arm lets hooks (e.g. an AI quota guard in `onBeforeStart`)
|
|
379
392
|
* reject a start with a domain-specific error key the engine just passes through.
|
|
380
393
|
*/
|
|
381
|
-
type FlowError = WorkflowNotFoundError | InvalidWorkflowDefinitionError | StepHandlerNotFoundError | WorkflowNotRetryableError | StepError | FlowErrorShape;
|
|
394
|
+
type FlowError = WorkflowNotFoundError | InvalidWorkflowDefinitionError | StepHandlerNotFoundError | WorkflowNotRetryableError | WorkflowStepRunningError | StepError | FlowErrorShape;
|
|
382
395
|
//#endregion
|
|
383
396
|
//#region src/core/defineStep.d.ts
|
|
384
397
|
/**
|
|
@@ -695,6 +708,176 @@ interface BuildWorkflowConfig<TInput extends Record<string, unknown>, TSteps ext
|
|
|
695
708
|
*/
|
|
696
709
|
declare function buildWorkflow<TInput extends Record<string, unknown>, TContext = unknown, TSteps extends Record<string, TypedStep<any, any, any, TContext>> = {}>(config: BuildWorkflowConfig<TInput, TSteps, TContext>): TypedWorkflow<TInput, TContext>;
|
|
697
710
|
//#endregion
|
|
711
|
+
//#region src/core/decision.d.ts
|
|
712
|
+
/** The questions a decision answers: each key names a question, each value its options. */
|
|
713
|
+
type DecisionQuestions = Readonly<Record<string, readonly [string, ...string[]]>>;
|
|
714
|
+
/** One answer: the chosen option, how sure the decider is, and optionally why. */
|
|
715
|
+
interface DecisionAnswer<TOption extends string = string> {
|
|
716
|
+
choice: TOption;
|
|
717
|
+
/** Probability in [0, 1] that `choice` is right. Only meaningful as a probability when the decider is `calibrated`. */
|
|
718
|
+
confidence: number;
|
|
719
|
+
/** The probability per option, where the decider gives one. */
|
|
720
|
+
distribution?: Partial<Record<TOption, number>>;
|
|
721
|
+
/** A short reason, where the decider gives one — shown to a reviewer, never parsed. */
|
|
722
|
+
reason?: string;
|
|
723
|
+
}
|
|
724
|
+
/** The answers to a set of questions, typed by their options. */
|
|
725
|
+
type DecisionAnswers<Q extends DecisionQuestions> = { [K in keyof Q]: DecisionAnswer<Q[K][number]>; };
|
|
726
|
+
/** The chosen option per question. */
|
|
727
|
+
type DecisionChoices<Q extends DecisionQuestions> = { [K in keyof Q]: Q[K][number]; };
|
|
728
|
+
/** What a decider is asked. */
|
|
729
|
+
interface DecisionRequest {
|
|
730
|
+
questions: DecisionQuestions;
|
|
731
|
+
/** The state to decide on — a data structure, not a prompt. */
|
|
732
|
+
input: unknown;
|
|
733
|
+
/** Optional guidance for the decider; a function decider may ignore it. */
|
|
734
|
+
instructions?: string;
|
|
735
|
+
signal?: AbortSignal;
|
|
736
|
+
}
|
|
737
|
+
/** What a decider answers: one answer per question. */
|
|
738
|
+
interface DecisionResponse {
|
|
739
|
+
answers: Record<string, DecisionAnswer>;
|
|
740
|
+
/** Overrides the decider's `id` for this answer — e.g. the model that actually ran. */
|
|
741
|
+
decider?: string;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Anything that can answer a decision. `calibrated` is the decider's claim
|
|
745
|
+
* that its confidences are probabilities — true for a deterministic function
|
|
746
|
+
* (always 1) or a model trained for it, false for a language model asked to
|
|
747
|
+
* state its own confidence. The step records it on every output, so a reader
|
|
748
|
+
* of the run knows how much a threshold meant.
|
|
749
|
+
*/
|
|
750
|
+
interface Decider<TContext = unknown> {
|
|
751
|
+
/** Recorded on every output — e.g. `function:rules` or `model:mistral-large`. */
|
|
752
|
+
readonly id: string;
|
|
753
|
+
readonly calibrated: boolean;
|
|
754
|
+
decide(request: DecisionRequest, context: TContext): Promise<DecisionResponse>;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* A decider over a plain function: every answer has confidence 1, so it never
|
|
758
|
+
* asks for review. For rules you can state, and for tests.
|
|
759
|
+
*/
|
|
760
|
+
declare function functionDecider<TContext = unknown>(id: string, decide: (request: DecisionRequest, context: TContext) => Record<string, string> | Promise<Record<string, string>>): Decider<TContext>;
|
|
761
|
+
/** What a decision step outputs. */
|
|
762
|
+
type DecisionOutput<Q extends DecisionQuestions> = {
|
|
763
|
+
answers: DecisionAnswers<Q>;
|
|
764
|
+
/** The decider's `id`. */
|
|
765
|
+
decider: string;
|
|
766
|
+
/** The decider's `calibrated` claim. */
|
|
767
|
+
calibrated: boolean;
|
|
768
|
+
/** The questions answered below their threshold — empty when none need a person. */
|
|
769
|
+
review: Array<keyof Q & string>;
|
|
770
|
+
};
|
|
771
|
+
/** A decision step: a `TypedStep` that also carries its questions, for the review and outcome steps. */
|
|
772
|
+
type DecisionStep<Q extends DecisionQuestions, TInput extends Record<string, unknown> = Record<string, unknown>, TDeps extends Record<string, TypedStep<any, any, any, TContext>> = {}, TContext = unknown> = TypedStep<TInput, DecisionOutput<Q>, TDeps, TContext> & {
|
|
773
|
+
readonly questions: Q;
|
|
774
|
+
};
|
|
775
|
+
/** Reviewing a decision: the options a person chose, for any subset of its questions. */
|
|
776
|
+
type DecisionReview<Q extends DecisionQuestions> = {
|
|
777
|
+
answers: Partial<DecisionChoices<Q>>;
|
|
778
|
+
/** Who reviewed — recorded on the outcome. */
|
|
779
|
+
reviewer?: string;
|
|
780
|
+
};
|
|
781
|
+
/** The settled decision: one option per question, and who chose it. */
|
|
782
|
+
type DecisionOutcome<Q extends DecisionQuestions> = {
|
|
783
|
+
choices: DecisionChoices<Q>;
|
|
784
|
+
decidedBy: { [K in keyof Q]: 'decider' | 'person'; };
|
|
785
|
+
decider: string;
|
|
786
|
+
reviewer?: string;
|
|
787
|
+
};
|
|
788
|
+
interface DefineDecisionStepConfig<Q extends DecisionQuestions, TInput extends Record<string, unknown>, TContext, TDeps extends Record<string, TypedStep<any, any, any, TContext>>, TJoin extends JoinRule> {
|
|
789
|
+
type: string;
|
|
790
|
+
workflowInputSchema: z.ZodType<TInput>;
|
|
791
|
+
dependencies?: TDeps;
|
|
792
|
+
/** The questions, each with its options — write them inline so their literals are kept. */
|
|
793
|
+
questions: Q;
|
|
794
|
+
/** The state the decider sees, built from the validated input and dependencies. */
|
|
795
|
+
input: (ctx: TypedStepContext<TInput, TDeps, TContext, TJoin>) => unknown;
|
|
796
|
+
/** Guidance passed to the decider. */
|
|
797
|
+
instructions?: string;
|
|
798
|
+
decider: Decider<TContext>;
|
|
799
|
+
/**
|
|
800
|
+
* Below this confidence a question is flagged for review — one number for
|
|
801
|
+
* every question, or one per question (a question left out is never
|
|
802
|
+
* flagged). Omitted: nothing is flagged.
|
|
803
|
+
*/
|
|
804
|
+
threshold?: number | Partial<Record<keyof Q, number>>;
|
|
805
|
+
/** Guard — see `defineStep`. */
|
|
806
|
+
when?: (ctx: TypedStepContext<TInput, TDeps, TContext, TJoin>) => boolean | Promise<boolean>;
|
|
807
|
+
/** Join rule — see `defineStep`. */
|
|
808
|
+
join?: TJoin;
|
|
809
|
+
retry?: RetryPolicy;
|
|
810
|
+
isRetryable?: (error: unknown) => boolean;
|
|
811
|
+
timeoutMs?: number;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Define a **decision** step: the decider answers each question with one of
|
|
815
|
+
* its options and a confidence; the output is typed by the options and
|
|
816
|
+
* validated against them, so a dependent can branch on it with a plain `when`.
|
|
817
|
+
*
|
|
818
|
+
* ```ts
|
|
819
|
+
* const triage = defineDecisionStep({
|
|
820
|
+
* type: 'mail:triage',
|
|
821
|
+
* workflowInputSchema: input,
|
|
822
|
+
* dependencies: { parseMail },
|
|
823
|
+
* questions: {
|
|
824
|
+
* tone: ['aggressive', 'friendly'],
|
|
825
|
+
* audience: ['invoicing', 'customer-support', 'tech-support'],
|
|
826
|
+
* },
|
|
827
|
+
* input: (ctx) => ctx.deps.parseMail,
|
|
828
|
+
* decider: createModelDecider(),
|
|
829
|
+
* threshold: 0.8,
|
|
830
|
+
* });
|
|
831
|
+
* ```
|
|
832
|
+
*
|
|
833
|
+
* Questions under `threshold` land in `output.review`; pair the step with
|
|
834
|
+
* {@link defineDecisionReviewStep} and {@link defineDecisionOutcomeStep} to have
|
|
835
|
+
* a person settle them.
|
|
836
|
+
*/
|
|
837
|
+
declare function defineDecisionStep<const Q extends DecisionQuestions, TInput extends Record<string, unknown>, TContext = unknown, TDeps extends Record<string, TypedStep<any, any, any, TContext>> = {}, TJoin extends JoinRule = 'all'>(config: DefineDecisionStepConfig<Q, TInput, TContext, TDeps, TJoin>): DecisionStep<Q, TInput, TDeps, TContext>;
|
|
838
|
+
/**
|
|
839
|
+
* Define the **review** of a decision: a wait step that suspends only when the
|
|
840
|
+
* decision flagged a question, until `engine.resumeStep` delivers a
|
|
841
|
+
* {@link DecisionReview} — the options a person chose. When nothing was
|
|
842
|
+
* flagged the step is skipped.
|
|
843
|
+
*
|
|
844
|
+
* `onTimeout: 'accept'` settles an expired review by keeping the decider's
|
|
845
|
+
* answers; the default `'fail'` ends the run.
|
|
846
|
+
*/
|
|
847
|
+
declare function defineDecisionReviewStep<Q extends DecisionQuestions, TKey extends string, TContext = unknown>(config: {
|
|
848
|
+
type: string;
|
|
849
|
+
/** The decision under review, keyed by its step key: `{ triage }`. */
|
|
850
|
+
decision: { [K in TKey]: DecisionStep<Q, any, any, TContext>; };
|
|
851
|
+
timeoutMs?: number;
|
|
852
|
+
onTimeout?: 'fail' | 'accept';
|
|
853
|
+
}): TypedStep<Record<string, never>, DecisionReview<Q>, { [K in TKey]: DecisionStep<Q, any, any, TContext>; }, TContext>;
|
|
854
|
+
/**
|
|
855
|
+
* Merge a decision with its review: a person's choice wins where they made
|
|
856
|
+
* one, the decider's stands everywhere else. Also what
|
|
857
|
+
* {@link defineDecisionOutcomeStep} runs.
|
|
858
|
+
*/
|
|
859
|
+
declare function resolveDecision<Q extends DecisionQuestions>(decision: DecisionOutput<Q>, review?: DecisionReview<Q>): DecisionOutcome<Q>;
|
|
860
|
+
/**
|
|
861
|
+
* Define the **outcome** of a decision: one step that waits for the review
|
|
862
|
+
* when there is one, runs straight after the decision when there is not, and
|
|
863
|
+
* outputs the settled {@link DecisionOutcome}. Branch on it downstream:
|
|
864
|
+
*
|
|
865
|
+
* ```ts
|
|
866
|
+
* const invoicing = defineStep({
|
|
867
|
+
* dependencies: { outcome },
|
|
868
|
+
* when: (ctx) => ctx.deps.outcome.choices.audience === 'invoicing',
|
|
869
|
+
* // …
|
|
870
|
+
* });
|
|
871
|
+
* ```
|
|
872
|
+
*/
|
|
873
|
+
declare function defineDecisionOutcomeStep<Q extends DecisionQuestions, TDecisionKey extends string, TReviewKey extends string, TContext = unknown>(config: {
|
|
874
|
+
type: string;
|
|
875
|
+
/** The decision, keyed by its step key: `{ triage }`. */
|
|
876
|
+
decision: { [K in TDecisionKey]: DecisionStep<Q, any, any, TContext>; };
|
|
877
|
+
/** Its review, keyed by its step key: `{ triageReview }`. */
|
|
878
|
+
review: { [K in TReviewKey]: TypedStep<any, DecisionReview<Q>, any, TContext>; };
|
|
879
|
+
}): TypedStep<Record<string, never>, DecisionOutcome<Q>, { [K in TDecisionKey]: DecisionStep<Q, any, any, TContext>; } & { [K in TReviewKey]: TypedStep<any, DecisionReview<Q>, any, TContext>; }, TContext>;
|
|
880
|
+
//#endregion
|
|
698
881
|
//#region src/core/public-view.d.ts
|
|
699
882
|
/** The five states a UI renders. Suspensions display as in-flight. */
|
|
700
883
|
type StepDisplayStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
@@ -823,4 +1006,4 @@ declare function toPublicStep(step: StepRecord): PublicWorkflowStep;
|
|
|
823
1006
|
*/
|
|
824
1007
|
declare function toPublicWorkflow(workflow: WorkflowWithSteps): PublicWorkflow;
|
|
825
1008
|
//#endregion
|
|
826
|
-
export {
|
|
1009
|
+
export { StepId as $, TypedStep as A, FlowError as B, defineDecisionOutcomeStep as C, resolveDecision as D, functionDecider as E, defineMapStep as F, StepCompensationContext as G, RetryPolicy as H, defineSleepStep as I, StepError as J, StepConditionHandler as K, defineStep as L, TypedWorkflow as M, WorkflowOutput as N, StepDeps as O, buildWorkflow as P, StepHandlerRegistry as Q, defineSubWorkflowStep as R, DecisionStep as S, defineDecisionStep as T, StartOptions as U, InvalidWorkflowDefinitionError as V, StepCompensateHandler as W, StepHandler as X, StepExecutionContext as Y, StepHandlerNotFoundError as Z, DecisionOutput as _, isTerminalStepStatus as _t, STEP_DISPLAY_STATUS as a, WorkflowDefinition as at, DecisionResponse as b, err as bt, WORKFLOW_STATUS_SCHEMA as c, WorkflowNotRetryableError as ct, toPublicWorkflow as d, WorkflowStepRunningError as dt, StepRecord as et, Decider as f, WorkflowWithSteps as ft, DecisionOutcome as g, computeReadiness as gt, DecisionChoices as h, ReadinessPlan as ht, PublicWorkflowStep as i, WorkflowCreatedResult as it, TypedStepContext as j, StepOutput as k, toDisplayStepStatus as l, WorkflowRecord as lt, DecisionAnswers as m, PlannedSkip as mt, PUBLIC_WORKFLOW_STEP_SCHEMA as n, StepStatus as nt, STEP_DISPLAY_STATUS_SCHEMA as o, WorkflowId as ot, DecisionAnswer as p, JoinRule as pt, StepDefinition as q, PublicWorkflow as r, WaitTimeoutPolicy as rt, StepDisplayStatus as s, WorkflowNotFoundError as st, PUBLIC_WORKFLOW_SCHEMA as t, StepRegistration as tt, toPublicStep as u, WorkflowStatus as ut, DecisionQuestions as v, FlowErrorShape as vt, defineDecisionReviewStep as w, DecisionReview as x, ok as xt, DecisionRequest as y, Result as yt, defineWaitStep as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as StepId, A as TypedStep, B as FlowError, C as defineDecisionOutcomeStep, D as resolveDecision, E as functionDecider, F as defineMapStep, G as StepCompensationContext, H as RetryPolicy, I as defineSleepStep, J as StepError, K as StepConditionHandler, L as defineStep, M as TypedWorkflow, N as WorkflowOutput, O as StepDeps, P as buildWorkflow, Q as StepHandlerRegistry, R as defineSubWorkflowStep, S as DecisionStep, T as defineDecisionStep, U as StartOptions, V as InvalidWorkflowDefinitionError, W as StepCompensateHandler, X as StepHandler, Y as StepExecutionContext, Z as StepHandlerNotFoundError, _ as DecisionOutput, _t as isTerminalStepStatus, a as STEP_DISPLAY_STATUS, at as WorkflowDefinition, b as DecisionResponse, bt as err, c as WORKFLOW_STATUS_SCHEMA, ct as WorkflowNotRetryableError, d as toPublicWorkflow, dt as WorkflowStepRunningError, et as StepRecord, f as Decider, ft as WorkflowWithSteps, g as DecisionOutcome, gt as computeReadiness, h as DecisionChoices, ht as ReadinessPlan, i as PublicWorkflowStep, it as WorkflowCreatedResult, j as TypedStepContext, k as StepOutput, l as toDisplayStepStatus, lt as WorkflowRecord, m as DecisionAnswers, mt as PlannedSkip, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepStatus, o as STEP_DISPLAY_STATUS_SCHEMA, ot as WorkflowId, p as DecisionAnswer, pt as JoinRule, q as StepDefinition, r as PublicWorkflow, rt as WaitTimeoutPolicy, s as StepDisplayStatus, st as WorkflowNotFoundError, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepRegistration, u as toPublicStep, ut as WorkflowStatus, v as DecisionQuestions, vt as FlowErrorShape, w as defineDecisionReviewStep, x as DecisionReview, xt as ok, y as DecisionRequest, yt as Result, z as defineWaitStep } from "./index-B8SvtiYn.js";
|
|
2
2
|
import { n as Logger, r as noopLogger, t as LogAttributes } from "./logger-BCus3AOa.js";
|
|
3
|
-
import { A as ListWorkflowsFilters, C as AddChildStep, D as CreatedWorkflow, E as CreateWorkflowStep, M as TransactionalScope, N as WorkflowStore, O as FailStepParams, S as createInMemoryStepGate, T as CreateWorkflowParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ReopenWorkflowParams, k as FinishWorkflowParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as CompleteStepParams, x as StepGateRequest, y as StepGate } from "./observability-
|
|
4
|
-
import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-
|
|
5
|
-
import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-
|
|
3
|
+
import { A as ListWorkflowsFilters, C as AddChildStep, D as CreatedWorkflow, E as CreateWorkflowStep, M as TransactionalScope, N as WorkflowStore, O as FailStepParams, P as WorkflowTreeFilter, S as createInMemoryStepGate, T as CreateWorkflowParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ReopenWorkflowParams, k as FinishWorkflowParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as CompleteStepParams, x as StepGateRequest, y as StepGate } from "./observability-DRUJOpiQ.js";
|
|
4
|
+
import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-B4haXU4w.js";
|
|
5
|
+
import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-BV-Vany9.js";
|
|
6
6
|
//#region src/core/registry.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Creates an in-memory step handler registry.
|
|
@@ -114,6 +114,11 @@ export interface RecoverySweepResult {
|
|
|
114
114
|
/** Workflows failed for outliving `StartOptions.timeoutMs`. */
|
|
115
115
|
expiredWorkflows: number;
|
|
116
116
|
}
|
|
117
|
+
/** What `purgeWorkflows` deleted. */
|
|
118
|
+
export interface PurgeWorkflowsResult {
|
|
119
|
+
/** Every run deleted, the sub-workflow runs below the matching ones included, ascending. */
|
|
120
|
+
workflowIds: WorkflowId[];
|
|
121
|
+
}
|
|
117
122
|
/** What `retryWorkflow` put back in flight. */
|
|
118
123
|
export interface RetryWorkflowResult {
|
|
119
124
|
workflowId: WorkflowId;
|
|
@@ -137,6 +142,7 @@ export declare function createWorkflowEngine<TContext = unknown>(deps: WorkflowE
|
|
|
137
142
|
recoverStuckWorkflows: () => Promise<RecoverySweepResult>;
|
|
138
143
|
retryWorkflow: (workflowId: WorkflowId) => Promise<Result<RetryWorkflowResult, FlowError>>;
|
|
139
144
|
cancelWorkflow: (workflowId: WorkflowId) => Promise<Result<void, FlowError>>;
|
|
145
|
+
purgeWorkflows: (filter: WorkflowTreeFilter) => Promise<Result<PurgeWorkflowsResult, FlowError>>;
|
|
140
146
|
getWorkflowStatus: (workflowId: WorkflowId) => Promise<Result<WorkflowWithSteps, FlowError>>;
|
|
141
147
|
listWorkflows: (filters?: {
|
|
142
148
|
status?: WorkflowStatus;
|
|
@@ -162,4 +168,4 @@ export declare function createInMemoryWorkflowStore(partitionKey?: string): Work
|
|
|
162
168
|
};
|
|
163
169
|
};
|
|
164
170
|
//#endregion
|
|
165
|
-
export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, DispatchKind, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowMeter, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, JoinRule, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PlannedSkip, PublicWorkflow, PublicWorkflowStep, RateRule, ReadinessPlan, RecordedSpan, RecordingObserver, RecordingTracer, ReopenWorkflowParams, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowNotRetryableError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStore, WorkflowWithSteps, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createMeterObserver, createRecordingObserver, createRecordingTracer, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|
|
171
|
+
export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, Decider, DecisionAnswer, DecisionAnswers, DecisionChoices, DecisionOutcome, DecisionOutput, DecisionQuestions, DecisionRequest, DecisionResponse, DecisionReview, DecisionStep, DispatchKind, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowMeter, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, JoinRule, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PlannedSkip, PublicWorkflow, PublicWorkflowStep, RateRule, ReadinessPlan, RecordedSpan, RecordingObserver, RecordingTracer, ReopenWorkflowParams, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowNotRetryableError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStepRunningError, WorkflowStore, WorkflowTreeFilter, WorkflowWithSteps, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createMeterObserver, createRecordingObserver, createRecordingTracer, defineDecisionOutcomeStep, defineDecisionReviewStep, defineDecisionStep, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, functionDecider, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, resolveDecision, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as nonRetryableError, D as ok, E as err, S as markRetryable, T as noopLogger, _ as defineStep, a as WORKFLOW_STATUS_SCHEMA, b as explicitRetryability, c as toPublicWorkflow, d as defineDecisionStep, f as functionDecider, g as defineSleepStep, h as defineMapStep, i as STEP_DISPLAY_STATUS_SCHEMA, l as defineDecisionOutcomeStep, m as buildWorkflow, n as PUBLIC_WORKFLOW_STEP_SCHEMA, o as toDisplayStepStatus, p as resolveDecision, r as STEP_DISPLAY_STATUS, s as toPublicStep, t as PUBLIC_WORKFLOW_SCHEMA, u as defineDecisionReviewStep, v as defineSubWorkflowStep, w as retryableError, x as isRetryableError, y as defineWaitStep } from "./core-C9h3-KFr.js";
|
|
2
2
|
//#region src/core/readiness.ts
|
|
3
3
|
/** Statuses a step can no longer move out of on its own. */
|
|
4
4
|
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -1715,6 +1715,60 @@ function createWorkflowEngine(deps) {
|
|
|
1715
1715
|
value: void 0
|
|
1716
1716
|
};
|
|
1717
1717
|
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Delete every run about one entity — the runs whose `entityRef` matches and every
|
|
1720
|
+
* sub-workflow run below them — with all of their steps: what a model was given and
|
|
1721
|
+
* what it said goes with them. For erasing a person, or deleting the record the runs
|
|
1722
|
+
* were about.
|
|
1723
|
+
*
|
|
1724
|
+
* Refuses with `workflow_step_running` while any of those runs has a step executing
|
|
1725
|
+
* (`running`, or `compensating`): a worker is inside its handler, and deleting the run
|
|
1726
|
+
* under it would let it write about something that no longer exists. Nothing is
|
|
1727
|
+
* cancelled or deleted then; try again once the step settles. A run that only *waits*
|
|
1728
|
+
* — for a person (`waiting`), a sleep, a sub-workflow, queued work — does not stop it.
|
|
1729
|
+
*
|
|
1730
|
+
* Otherwise every active run is cancelled first, parents before children, exactly as
|
|
1731
|
+
* `cancelWorkflow` would (a matching sub-workflow run whose parent is not being purged
|
|
1732
|
+
* fails the parent's step), and then all of them are deleted. Whatever was still queued
|
|
1733
|
+
* for them — a step job, a wait deadline, a `resumeStep` — finds no run and ends in
|
|
1734
|
+
* `workflow_not_found`; nothing is written back. `workflow.cancelled` and then
|
|
1735
|
+
* `workflow.purged` are emitted per run.
|
|
1736
|
+
*
|
|
1737
|
+
* Runs, not history: rows a host wrote elsewhere keyed by the run id (an event sink's
|
|
1738
|
+
* `flow_step_event`, a ledger entry, a usage rollup) are the host's to keep or delete —
|
|
1739
|
+
* the ids come back for that.
|
|
1740
|
+
*/
|
|
1741
|
+
async function purgeWorkflows(filter) {
|
|
1742
|
+
const tree = await store.listWorkflowTree(filter);
|
|
1743
|
+
if (tree.length === 0) return {
|
|
1744
|
+
ok: true,
|
|
1745
|
+
value: { workflowIds: [] }
|
|
1746
|
+
};
|
|
1747
|
+
const running = tree.flatMap((w) => w.steps.filter((s) => s.status === "running" || s.status === "compensating").map((s) => ({
|
|
1748
|
+
workflowId: w.id,
|
|
1749
|
+
stepKey: s.key,
|
|
1750
|
+
status: s.status
|
|
1751
|
+
})));
|
|
1752
|
+
if (running.length > 0) return {
|
|
1753
|
+
ok: false,
|
|
1754
|
+
error: {
|
|
1755
|
+
key: "workflow_step_running",
|
|
1756
|
+
running,
|
|
1757
|
+
message: `A step is executing in ${[...new Set(running.map((r) => r.workflowId))].map((id) => `run ${id}`).join(", ")}; try again once it settles`
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
for (const w of tree) if (w.status === "pending" || w.status === "running") await cancelWorkflow(w.id);
|
|
1761
|
+
const workflowIds = await store.deleteWorkflows(filter);
|
|
1762
|
+
logger.info("Workflows purged", { count: workflowIds.length });
|
|
1763
|
+
for (const workflowId of workflowIds) emit({
|
|
1764
|
+
type: "workflow.purged",
|
|
1765
|
+
workflowId
|
|
1766
|
+
});
|
|
1767
|
+
return {
|
|
1768
|
+
ok: true,
|
|
1769
|
+
value: { workflowIds }
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1718
1772
|
async function getWorkflowStatus(workflowId) {
|
|
1719
1773
|
const workflow = await store.getWorkflow(workflowId);
|
|
1720
1774
|
if (!workflow) return {
|
|
@@ -1945,6 +1999,7 @@ function createWorkflowEngine(deps) {
|
|
|
1945
1999
|
recoverStuckWorkflows,
|
|
1946
2000
|
retryWorkflow,
|
|
1947
2001
|
cancelWorkflow,
|
|
2002
|
+
purgeWorkflows,
|
|
1948
2003
|
getWorkflowStatus,
|
|
1949
2004
|
listWorkflows
|
|
1950
2005
|
};
|
|
@@ -2190,6 +2245,35 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
|
|
|
2190
2245
|
steps: stepsOf(w.id).map(clone)
|
|
2191
2246
|
}));
|
|
2192
2247
|
}
|
|
2248
|
+
/** The runs about the entity and every sub-workflow run below them, ascending by id. */
|
|
2249
|
+
function treeOf(filter) {
|
|
2250
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2251
|
+
for (const w of workflows.values()) if (w.entityRef === filter.entityRef) ids.add(w.id);
|
|
2252
|
+
let grew = ids.size > 0;
|
|
2253
|
+
while (grew) {
|
|
2254
|
+
grew = false;
|
|
2255
|
+
for (const w of workflows.values()) if (w.parentWorkflowId != null && ids.has(w.parentWorkflowId) && !ids.has(w.id)) {
|
|
2256
|
+
ids.add(w.id);
|
|
2257
|
+
grew = true;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return Array.from(ids, (id) => workflows.get(id)).sort((a, b) => a.id - b.id);
|
|
2261
|
+
}
|
|
2262
|
+
async function listWorkflowTree(filter) {
|
|
2263
|
+
return treeOf(filter).map((w) => ({
|
|
2264
|
+
...clone(w),
|
|
2265
|
+
steps: stepsOf(w.id).map(clone)
|
|
2266
|
+
}));
|
|
2267
|
+
}
|
|
2268
|
+
async function deleteWorkflows(filter) {
|
|
2269
|
+
const doomed = treeOf(filter);
|
|
2270
|
+
for (const w of doomed) {
|
|
2271
|
+
for (const s of stepsOf(w.id)) steps.delete(s.id);
|
|
2272
|
+
workflows.delete(w.id);
|
|
2273
|
+
if (w.idempotencyKey != null && byIdempotency.get(w.idempotencyKey) === w.id) byIdempotency.delete(w.idempotencyKey);
|
|
2274
|
+
}
|
|
2275
|
+
return doomed.map((w) => w.id);
|
|
2276
|
+
}
|
|
2193
2277
|
async function listRunningWorkflows() {
|
|
2194
2278
|
return Array.from(workflows.values()).filter((w) => w.status === "running").map(clone);
|
|
2195
2279
|
}
|
|
@@ -2222,6 +2306,8 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
|
|
|
2222
2306
|
skipPendingSteps,
|
|
2223
2307
|
finishWorkflow,
|
|
2224
2308
|
listWorkflows,
|
|
2309
|
+
listWorkflowTree,
|
|
2310
|
+
deleteWorkflows,
|
|
2225
2311
|
listRunningWorkflows,
|
|
2226
2312
|
findStuckSteps,
|
|
2227
2313
|
_dump() {
|
|
@@ -2233,4 +2319,4 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
|
|
|
2233
2319
|
};
|
|
2234
2320
|
}
|
|
2235
2321
|
//#endregion
|
|
2236
|
-
export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, WORKFLOW_STATUS_SCHEMA, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createInMemoryWorkflowStore, createMeterObserver, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, isRetryableError, isTerminalStepStatus, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|
|
2322
|
+
export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, WORKFLOW_STATUS_SCHEMA, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createInMemoryWorkflowStore, createMeterObserver, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineDecisionOutcomeStep, defineDecisionReviewStep, defineDecisionStep, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, functionDecider, isRetryableError, isTerminalStepStatus, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, resolveDecision, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as StepId, et as StepRecord, ft as WorkflowWithSteps, lt as WorkflowRecord, ot as WorkflowId, ut as WorkflowStatus } from "./index-B8SvtiYn.js";
|
|
2
2
|
//#region src/core/store.d.ts
|
|
3
3
|
interface CreateWorkflowStep {
|
|
4
4
|
key: string;
|
|
@@ -75,6 +75,15 @@ interface ListWorkflowsFilters {
|
|
|
75
75
|
entityRef?: string;
|
|
76
76
|
limit?: number;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The runs {@link WorkflowStore.listWorkflowTree} and {@link WorkflowStore.deleteWorkflows}
|
|
80
|
+
* address: every run of the store's partition whose `entityRef` matches, **plus every
|
|
81
|
+
* sub-workflow run below them**, at any depth — a child run carries its parent's id, not
|
|
82
|
+
* its entity, so it would otherwise be missed.
|
|
83
|
+
*/
|
|
84
|
+
interface WorkflowTreeFilter {
|
|
85
|
+
entityRef: string;
|
|
86
|
+
}
|
|
78
87
|
/**
|
|
79
88
|
* A store bound to an open transaction, plus an opaque handle a capable
|
|
80
89
|
* {@link Dispatcher} can join so a job and the state change that produced it
|
|
@@ -197,6 +206,22 @@ interface WorkflowStore {
|
|
|
197
206
|
reopenWorkflow(params: ReopenWorkflowParams): Promise<void>;
|
|
198
207
|
/** List workflows (with their steps) for status/dashboard reads. */
|
|
199
208
|
listWorkflows(filters: ListWorkflowsFilters): Promise<WorkflowWithSteps[]>;
|
|
209
|
+
/**
|
|
210
|
+
* Every run the filter addresses (see {@link WorkflowTreeFilter}), with its steps,
|
|
211
|
+
* ordered by id — so a parent always comes before its children. No limit: the engine
|
|
212
|
+
* reads this to decide whether the whole set may go, and a page would hide a run.
|
|
213
|
+
*/
|
|
214
|
+
listWorkflowTree(filter: WorkflowTreeFilter): Promise<WorkflowWithSteps[]>;
|
|
215
|
+
/**
|
|
216
|
+
* Delete every run the filter addresses, with all of their steps (map children
|
|
217
|
+
* included), and return the ids of the runs deleted, ascending.
|
|
218
|
+
*
|
|
219
|
+
* Implementations MUST delete the runs and their steps together — one statement or
|
|
220
|
+
* one transaction — so no step row outlives its run, whatever foreign keys the host
|
|
221
|
+
* declared. Nothing is checked here: whether a run may go is the engine's question
|
|
222
|
+
* (`purgeWorkflows`), asked before it calls this.
|
|
223
|
+
*/
|
|
224
|
+
deleteWorkflows(filter: WorkflowTreeFilter): Promise<WorkflowId[]>;
|
|
200
225
|
/** All workflows currently in `running` state (for the stuck-step sweeper). */
|
|
201
226
|
listRunningWorkflows(): Promise<WorkflowRecord[]>;
|
|
202
227
|
/**
|
|
@@ -275,7 +300,12 @@ declare function createInMemoryStepGate(config?: InMemoryStepGateConfig): StepGa
|
|
|
275
300
|
/** Lifecycle event types the engine emits at every workflow/step transition. */
|
|
276
301
|
type FlowEventType = 'workflow.started' | 'workflow.completed' | 'workflow.failed' | 'workflow.cancelled' |
|
|
277
302
|
/** A `failed` workflow was put back in flight by `retryWorkflow`. */
|
|
278
|
-
'workflow.retried' |
|
|
303
|
+
'workflow.retried' |
|
|
304
|
+
/**
|
|
305
|
+
* The run was deleted by `purgeWorkflows`, with its steps. Emitted after the delete,
|
|
306
|
+
* once per run; nothing about the run can be read any more.
|
|
307
|
+
*/
|
|
308
|
+
'workflow.purged' | 'step.started' | 'step.completed' | 'step.failed' | 'step.retrying' | 'step.skipped' | 'step.waiting' | 'step.resumed' |
|
|
279
309
|
/**
|
|
280
310
|
* A suspended step's wait budget elapsed. Always followed by the event for
|
|
281
311
|
* what the policy did — `step.failed`, or `step.completed` for `{ output }`.
|
|
@@ -381,4 +411,4 @@ interface RecordingTracer extends FlowTracer {
|
|
|
381
411
|
}
|
|
382
412
|
declare function createRecordingTracer(): RecordingTracer;
|
|
383
413
|
//#endregion
|
|
384
|
-
export { ListWorkflowsFilters as A, AddChildStep as C, CreatedWorkflow as D, CreateWorkflowStep as E, TransactionalScope as M, WorkflowStore as N, FailStepParams as O, createInMemoryStepGate as S, CreateWorkflowParams as T, InMemoryStepGateConfig as _, FlowSpan as a, StepGateDecision as b, RecordingObserver as c, createMeterObserver as d, createRecordingObserver as f, ConcurrencyRule as g, noopTracer as h, FlowObserver as i, ReopenWorkflowParams as j, FinishWorkflowParams as k, RecordingTracer as l, noopObserver as m, FlowEventType as n, FlowTracer as o, createRecordingTracer as p, FlowMeter as r, RecordedSpan as s, FlowEvent as t, combineObservers as u, RateRule as v, CompleteStepParams as w, StepGateRequest as x, StepGate as y };
|
|
414
|
+
export { ListWorkflowsFilters as A, AddChildStep as C, CreatedWorkflow as D, CreateWorkflowStep as E, TransactionalScope as M, WorkflowStore as N, FailStepParams as O, WorkflowTreeFilter as P, createInMemoryStepGate as S, CreateWorkflowParams as T, InMemoryStepGateConfig as _, FlowSpan as a, StepGateDecision as b, RecordingObserver as c, createMeterObserver as d, createRecordingObserver as f, ConcurrencyRule as g, noopTracer as h, FlowObserver as i, ReopenWorkflowParams as j, FinishWorkflowParams as k, RecordingTracer as l, noopObserver as m, FlowEventType as n, FlowTracer as o, createRecordingTracer as p, FlowMeter as r, RecordedSpan as s, FlowEvent as t, combineObservers as u, RateRule as v, CompleteStepParams as w, StepGateRequest as x, StepGate as y };
|
package/dist/pg-boss/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import "../index-
|
|
1
|
+
import "../index-B8SvtiYn.js";
|
|
2
2
|
import { n as Logger } from "../logger-BCus3AOa.js";
|
|
3
|
-
import { r as Dispatcher } from "../dispatcher-
|
|
3
|
+
import { r as Dispatcher } from "../dispatcher-B4haXU4w.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { PgBoss } from "pg-boss";
|
|
6
6
|
//#region src/pg-boss/payload.d.ts
|
package/dist/postgres/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { N as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-
|
|
1
|
+
import { ot as WorkflowId } from "../index-B8SvtiYn.js";
|
|
2
|
+
import { N as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-DRUJOpiQ.js";
|
|
3
3
|
import { Pool } from "pg";
|
|
4
4
|
//#region src/postgres/ddl.d.ts
|
|
5
5
|
/**
|
package/dist/postgres/index.js
CHANGED
|
@@ -410,9 +410,12 @@ function createWorkflowStore(deps) {
|
|
|
410
410
|
}
|
|
411
411
|
args.push(filters.limit ?? 50);
|
|
412
412
|
const limitIdx = args.length;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
return withSteps((await exec.query(`SELECT * FROM ${WF} WHERE ${conds.join(" AND ")} ORDER BY id DESC LIMIT $${limitIdx}`, args)).rows);
|
|
414
|
+
}
|
|
415
|
+
/** Attach each run's steps in one query. */
|
|
416
|
+
async function withSteps(rows) {
|
|
417
|
+
if (rows.length === 0) return [];
|
|
418
|
+
const ids = rows.map((r) => Number(r.id));
|
|
416
419
|
const stepRes = await exec.query(`SELECT * FROM ${STEP} WHERE workflow_id = ANY($1::bigint[]) AND partition_key = $2 ORDER BY id`, [ids, partitionKey]);
|
|
417
420
|
const stepsByWf = /* @__PURE__ */ new Map();
|
|
418
421
|
for (const row of stepRes.rows) {
|
|
@@ -421,11 +424,33 @@ function createWorkflowStore(deps) {
|
|
|
421
424
|
list.push(s);
|
|
422
425
|
stepsByWf.set(s.workflowId, list);
|
|
423
426
|
}
|
|
424
|
-
return
|
|
427
|
+
return rows.map((r) => ({
|
|
425
428
|
...mapWorkflow(r),
|
|
426
429
|
steps: stepsByWf.get(Number(r.id)) ?? []
|
|
427
430
|
}));
|
|
428
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* The runs about the entity and every sub-workflow run below them, as a recursive CTE
|
|
434
|
+
* over `parent_workflow_id` (`$1` partition, `$2` entity). Shared by the read and the
|
|
435
|
+
* delete so the two can never address different sets.
|
|
436
|
+
*/
|
|
437
|
+
const TREE = `tree(id) AS (
|
|
438
|
+
SELECT id FROM ${WF} WHERE partition_key = $1 AND entity_ref = $2
|
|
439
|
+
UNION
|
|
440
|
+
SELECT c.id FROM ${WF} c JOIN tree t ON c.parent_workflow_id = t.id WHERE c.partition_key = $1
|
|
441
|
+
)`;
|
|
442
|
+
async function listWorkflowTree(filter) {
|
|
443
|
+
return withSteps((await exec.query(`WITH RECURSIVE ${TREE}
|
|
444
|
+
SELECT w.* FROM ${WF} w JOIN tree t ON t.id = w.id WHERE w.partition_key = $1 ORDER BY w.id`, [partitionKey, filter.entityRef])).rows);
|
|
445
|
+
}
|
|
446
|
+
async function deleteWorkflows(filter) {
|
|
447
|
+
return (await exec.query(`WITH RECURSIVE ${TREE},
|
|
448
|
+
gone_steps AS (
|
|
449
|
+
DELETE FROM ${STEP} s USING tree t WHERE s.workflow_id = t.id AND s.partition_key = $1
|
|
450
|
+
)
|
|
451
|
+
DELETE FROM ${WF} w USING tree t WHERE w.id = t.id AND w.partition_key = $1
|
|
452
|
+
RETURNING w.id`, [partitionKey, filter.entityRef])).rows.map((r) => Number(r.id)).sort((a, b) => a - b);
|
|
453
|
+
}
|
|
429
454
|
async function listRunningWorkflows() {
|
|
430
455
|
return (await exec.query(`SELECT * FROM ${WF} WHERE partition_key = $1 AND status = 'running'`, [partitionKey])).rows.map(mapWorkflow);
|
|
431
456
|
}
|
|
@@ -482,6 +507,8 @@ function createWorkflowStore(deps) {
|
|
|
482
507
|
skipPendingSteps,
|
|
483
508
|
finishWorkflow,
|
|
484
509
|
listWorkflows,
|
|
510
|
+
listWorkflowTree,
|
|
511
|
+
deleteWorkflows,
|
|
485
512
|
listRunningWorkflows,
|
|
486
513
|
findStuckSteps
|
|
487
514
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octaflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss, with an optional AI add-on (token/cost/quota). Layered single package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -116,18 +116,18 @@
|
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
|
-
"@ai-sdk/anthropic": "^4.0.
|
|
120
|
-
"@ai-sdk/mistral": "^4.0.
|
|
121
|
-
"@ai-sdk/openai": "^4.0.
|
|
122
|
-
"@ai-sdk/openai-compatible": "^3.0.
|
|
123
|
-
"@ai-sdk/provider": "^4.0.
|
|
119
|
+
"@ai-sdk/anthropic": "^4.0.71",
|
|
120
|
+
"@ai-sdk/mistral": "^4.0.56",
|
|
121
|
+
"@ai-sdk/openai": "^4.0.83",
|
|
122
|
+
"@ai-sdk/openai-compatible": "^3.0.62",
|
|
123
|
+
"@ai-sdk/provider": "^4.0.21",
|
|
124
124
|
"@testcontainers/postgresql": "^12.2.0",
|
|
125
|
-
"@types/node": "^26.6.
|
|
125
|
+
"@types/node": "^26.6.4",
|
|
126
126
|
"@types/pg": "^8.23.1",
|
|
127
|
-
"ai": "^7.0.
|
|
127
|
+
"ai": "^7.0.127",
|
|
128
128
|
"drizzle-orm": "^0.45.3",
|
|
129
129
|
"pg": "^8.23.1",
|
|
130
|
-
"pg-boss": "^12.35.
|
|
130
|
+
"pg-boss": "^12.35.1",
|
|
131
131
|
"testcontainers": "^12.2.0",
|
|
132
132
|
"tsdown": "^0.23.0",
|
|
133
133
|
"typescript": "^7.0.2",
|