octaflow 0.38.0 → 0.40.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 CHANGED
@@ -214,6 +214,8 @@ next to what your steps actually do.
214
214
  | ↩️ | **Saga compensation** — run rollback handlers in reverse order on failure |
215
215
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
216
216
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
217
+ | 📌 | **Step checkpoints** — `ctx.checkpoint`: a step keeps what it has done so far (`save`), and the attempt after a crash reads it (`value`) and goes on from there; written only by the attempt that owns the step |
218
+ | ⏭️ | **Continue later** — a handler throws `continueStep(afterMs)` to stop now and run again in a new invocation, without spending an attempt: work in slices on a host with a hard request ceiling, going on from `ctx.checkpoint` |
217
219
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
218
220
  | 📄 | **Workflow documents** (`octaflow/document`) — a workflow as JSON: a template a tenant saves or a model drafts, checked by a compiler (types, expressions, bounded waits, roles, no outward step without a person's approval) and run on one registration per kind, with no deploy per template. See [Workflow documents](#workflow-documents) |
219
221
  | 🤖 | **The model layer** — AI steps with token and cost capture, quotas, the model decider, agents that end by choosing an exit, case loops, managed providers and model runners: [`@octabits-io/models`](../models/README.md) (`./octaflow` is its binding to this engine; it was `octaflow/ai` until 0.36) |
@@ -263,8 +265,9 @@ decimals (`decimal(input.amount)`) as types of their own.
263
265
  | `repeat` | its steps pass after pass while a condition holds, up to a cap — one step of one run |
264
266
  | `flow` | groups steps, or runs another document (`ref`, pinned to a revision) as a sub-workflow |
265
267
 
266
- A host adds its own kinds; a kind can check its own params, name the expressions it keeps
267
- (`reads`), or stand for several steps (`expand`). Policy is reviewed between revisions:
268
+ A host adds its own kinds; a kind can check its own params (`check`, whose issues are `params`
269
+ unless one says `approval_before_impact` — an agent kind given an outward tool set, say), name
270
+ the expressions it keeps (`reads`), or stand for several steps (`expand`). Policy is reviewed between revisions:
268
271
  `reviewWorkflowRevision` lists what changed in it, and `checkPublication` wants someone other
269
272
  than the author to approve a change. The format and its decisions are in
270
273
  [docs/workflow-documents.md](./docs/workflow-documents.md).
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- //#region src/core/retry.ts
2
+ // src/core/retry.ts
3
3
  /**
4
4
  * Realm-safe marker for an explicit retryability decision. `Symbol.for` so a
5
5
  * duplicated copy of this module still reads markers set by the other copy.
@@ -133,8 +133,30 @@ function isRetryableError(error) {
133
133
  if (message.includes("503") || message.includes("service unavailable")) return true;
134
134
  return false;
135
135
  }
136
- //#endregion
137
- //#region src/core/defineStep.ts
136
+ const CONTINUE = Symbol.for("octaflow.continue");
137
+ /**
138
+ * Throw this from a handler to stop now and run again later, without spending
139
+ * an attempt: the engine puts the step back to `pending` and re-queues it
140
+ * after `afterMs`. For a handler that works in slices of wall-clock time — a
141
+ * host with a hard request ceiling — and keeps its progress in
142
+ * `ctx.checkpoint`, so the next run goes on from there. Nothing caps how
143
+ * often a step continues; the handler's own limits must.
144
+ */
145
+ function continueStep(afterMs = 0, message = "the step continues in its next slice") {
146
+ const error = new Error(message);
147
+ Object.defineProperty(error, CONTINUE, {
148
+ value: Math.max(0, afterMs),
149
+ enumerable: false
150
+ });
151
+ return error;
152
+ }
153
+ /** The delay a {@link continueStep} error asks for, or `undefined` for any other error. */
154
+ function continuationOf(error) {
155
+ if (typeof error !== "object" || error === null) return void 0;
156
+ const afterMs = error[CONTINUE];
157
+ return typeof afterMs === "number" ? afterMs : void 0;
158
+ }
159
+ // src/core/defineStep.ts
138
160
  /**
139
161
  * `z.object({})` typed as the empty-record schema. Zod infers `{}` for an empty object schema,
140
162
  * but we want `Record<string, never>` for input/output-less steps — hence the bridge cast.
@@ -172,11 +194,15 @@ function defineStep(config) {
172
194
  const toStepError = (error) => {
173
195
  const message = error instanceof Error ? error.message : "Unknown step error";
174
196
  const marked = explicitRetryability(error);
197
+ const retryable = marked ?? (isRetryable ? isRetryable(error) : isRetryableError(error));
198
+ const retryableFrom = marked !== void 0 ? "explicit" : isRetryable ? "predicate" : "heuristic";
199
+ const continueAfterMs = continuationOf(error);
175
200
  return {
176
201
  key: "step_error",
177
202
  message,
178
- retryable: marked ?? (isRetryable ? isRetryable(error) : isRetryableError(error)),
179
- retryableFrom: marked !== void 0 ? "explicit" : isRetryable ? "predicate" : "heuristic"
203
+ retryable,
204
+ retryableFrom,
205
+ ...continueAfterMs !== void 0 && { continueAfterMs }
180
206
  };
181
207
  };
182
208
  /**
@@ -224,6 +250,7 @@ function defineStep(config) {
224
250
  deps: parsedDeps,
225
251
  signal: ctx.signal,
226
252
  heartbeat: ctx.heartbeat,
253
+ checkpoint: ctx.checkpoint,
227
254
  context: ctx.context
228
255
  }
229
256
  };
@@ -525,8 +552,7 @@ function buildWorkflow(config) {
525
552
  }
526
553
  };
527
554
  }
528
- //#endregion
529
- //#region src/core/decision.ts
555
+ // src/core/decision.ts
530
556
  /**
531
557
  * A decider over a plain function: every answer has confidence 1, so it never
532
558
  * asks for review. For rules you can state, and for tests.
@@ -743,8 +769,7 @@ function defineDecisionOutcomeStep(config) {
743
769
  }
744
770
  });
745
771
  }
746
- //#endregion
747
- //#region src/core/public-view.ts
772
+ // src/core/public-view.ts
748
773
  /**
749
774
  * Public wire view of a workflow — the projection every HTTP consumer was
750
775
  * hand-writing.
@@ -881,5 +906,4 @@ function toPublicWorkflow(workflow) {
881
906
  completedAt: workflow.completedAt
882
907
  };
883
908
  }
884
- //#endregion
885
- export { nonRetryableError as C, markRetryable as S, 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 };
909
+ export { isRetryableError as C, retryableError as E, explicitRetryability as S, nonRetryableError as T, defineStep as _, WORKFLOW_STATUS_SCHEMA as a, continuationOf 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, markRetryable as w, continueStep as x, defineWaitStep as y };
@@ -1,5 +1,4 @@
1
- import { Tt as Result, ft as WorkflowId, rt as StepId, wt as FlowErrorShape } from "./index-BlwtmRJP.js";
2
- //#region src/core/logger.d.ts
1
+ import { Et as Result, Tt as FlowErrorShape, it as StepId, pt as WorkflowId } from "./index-BBO5BGS8.js";
3
2
  type LogAttributes = Record<string, unknown>;
4
3
  interface Logger {
5
4
  info(message: string, attributes?: LogAttributes): void;
@@ -7,8 +6,6 @@ interface Logger {
7
6
  error(message: string, error?: Error, attributes?: LogAttributes): void;
8
7
  }
9
8
  declare const noopLogger: Logger;
10
- //#endregion
11
- //#region src/core/dispatcher.d.ts
12
9
  /**
13
10
  * What a step job is for. A queue carries two kinds of work for a step, and
14
11
  * they differ only in this field:
@@ -69,5 +66,4 @@ interface Dispatcher {
69
66
  */
70
67
  enqueueStepIn?(handle: unknown, payload: DispatchStepPayload, options?: EnqueueOptions): Promise<Result<void, FlowErrorShape>>;
71
68
  }
72
- //#endregion
73
69
  export { LogAttributes as a, EnqueueOptions as i, DispatchStepPayload as n, Logger as o, Dispatcher as r, noopLogger as s, DispatchKind as t };
@@ -1,7 +1,6 @@
1
- import { H as FlowError, Q as StepExecutionContext, Tt as Result, W as RetryPolicy, dt as WorkflowDefinition, ft as WorkflowId, it as StepPolicy, nt as StepHandlerRegistry } from "../index-BlwtmRJP.js";
2
- import { f as WorkflowStore } from "../store-BPHVI_a3.js";
1
+ import { $ as StepExecutionContext, Et as Result, H as FlowError, W as RetryPolicy, at as StepPolicy, ft as WorkflowDefinition, pt as WorkflowId, rt as StepHandlerRegistry } from "../index-BBO5BGS8.js";
2
+ import { f as WorkflowStore } from "../store-oCpqAuTT.js";
3
3
  import { z } from "zod";
4
- //#region src/document/schema.d.ts
5
4
  /** A schema of the subset, or one the checker derived. `{}` is "anything". */
6
5
  type JsonSchema = {
7
6
  type?: 'object' | 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'null' | 'duration' | 'decimal';
@@ -29,8 +28,6 @@ export declare function checkSchemaSubset(schema: unknown, path: string): Pathed
29
28
  * Cached by the schema's text, so a document's steps share one validator each.
30
29
  */
31
30
  export declare function zodFor(schema: JsonSchema): z.ZodType;
32
- //#endregion
33
- //#region src/document/format.d.ts
34
31
  export declare const WORKFLOW_DOCUMENT_FORMAT: 'octaflow.workflow';
35
32
  export declare const WORKFLOW_DOCUMENT_VERSION: 1;
36
33
  interface DocumentStep {
@@ -48,6 +45,14 @@ interface DocumentStep {
48
45
  onFailure?: 'fail' | {
49
46
  output: Record<string, unknown>;
50
47
  };
48
+ /**
49
+ * For `map` only: what an item whose run is cancelled counts as — an item withdrawn on purpose
50
+ * (`cancelWorkflow` on its run). The stand-in takes its slot of `items` and the map goes on with
51
+ * the others. Without it a cancelled item fails the map; an item whose run *fails* fails it either way.
52
+ */
53
+ itemOnCancel?: {
54
+ output: Record<string, unknown>;
55
+ };
51
56
  retry?: {
52
57
  maxAttempts: number;
53
58
  backoff?: 'fixed' | 'exponential';
@@ -92,8 +97,6 @@ export declare const workflowDocumentSchema: z.ZodObject<{
92
97
  }, z.core.$strict>;
93
98
  type WorkflowDocument = z.infer<typeof workflowDocumentSchema>;
94
99
  type DocumentRole = z.infer<typeof roleSchema>;
95
- //#endregion
96
- //#region src/document/kinds.d.ts
97
100
  /** Who acts in a step of this kind; a step's `role` must allow it. */
98
101
  type KindActor = 'agent' | 'person' | 'system';
99
102
  /**
@@ -132,6 +135,8 @@ type KindCheck = (args: {
132
135
  }) => Array<{
133
136
  path: string;
134
137
  message: string;
138
+ /** Default `params`; `approval_before_impact` for a step that would act outward with no person's approval. */
139
+ code?: 'params' | 'approval_before_impact';
135
140
  }>;
136
141
  interface KindHandlerArgs<TParams, TContext> {
137
142
  /** The step's params, every binding resolved, parsed by the kind's schema. */
@@ -229,8 +234,6 @@ export declare const subflowKind: WorkflowKind<unknown>;
229
234
  */
230
235
  export declare const decideKind: WorkflowKind<unknown>;
231
236
  export declare const coreKinds: readonly WorkflowKind[];
232
- //#endregion
233
- //#region src/document/compile.d.ts
234
237
  interface CompileIssue extends PathedIssue {
235
238
  code: CompileIssueCode;
236
239
  }
@@ -287,8 +290,6 @@ interface InvalidWorkflowDocumentError {
287
290
  issues: CompileIssue[];
288
291
  }
289
292
  export declare function compileWorkflowDocument(raw: unknown, kinds: readonly WorkflowKind<any>[], options?: CompileOptions): Result<CompiledWorkflow, InvalidWorkflowDocumentError>;
290
- //#endregion
291
- //#region src/document/runtime.d.ts
292
293
  /**
293
294
  * The spec of a document step, from its record's input — what a model resolver
294
295
  * reads to pick the model for a step's `role` (`spec.params.role`). Null for a
@@ -340,8 +341,6 @@ export declare function resumeDocumentStep(engine: ResumableEngine, compiled: Pi
340
341
  export declare function resumeWaitingStep(engine: ResumableEngine, store: WorkflowStore, workflowId: WorkflowId, stepKey: string, answer: Record<string, unknown>): Promise<Result<void, ResumeDocumentStepError>>;
341
342
  /** The run's result, from its document's `result` expressions over the steps' outputs. */
342
343
  export declare function documentResult(store: WorkflowStore, compiled: Pick<CompiledWorkflow, 'result'>, workflowId: WorkflowId): Promise<Record<string, unknown> | null>;
343
- //#endregion
344
- //#region src/document/expr.d.ts
345
344
  type Expr = {
346
345
  kind: 'literal';
347
346
  value: string | number | boolean | null;
@@ -384,8 +383,6 @@ interface CheckResult {
384
383
  * that never fit. "Anything" passes, since it might.
385
384
  */
386
385
  export declare function checkExpr(e: Expr, scope: Record<string, JsonSchema>): CheckResult;
387
- //#endregion
388
- //#region src/document/duration.d.ts
389
386
  /** A duration as an expression value: tagged, so it never passes for a number. */
390
387
  interface Duration {
391
388
  readonly $duration: number;
@@ -394,8 +391,6 @@ export declare function duration(ms: number): Duration;
394
391
  export declare function isDuration(value: unknown): value is Duration;
395
392
  /** An ISO 8601 duration (`P2D`, `PT4H30M`) in ms, or null when it isn't one this format accepts. */
396
393
  export declare function parseIsoDuration(text: string): number | null;
397
- //#endregion
398
- //#region src/document/decimal.d.ts
399
394
  /** A decimal as an expression value: tagged, so it never passes for a number or a string. */
400
395
  interface Decimal {
401
396
  readonly $decimal: string;
@@ -404,8 +399,6 @@ export declare const DIVISION_SCALE = 9;
404
399
  export declare function isDecimal(value: unknown): value is Decimal;
405
400
  /** A decimal from a decimal string or a finite number; null for anything else. */
406
401
  export declare function toDecimal(value: unknown): Decimal | null;
407
- //#endregion
408
- //#region src/document/policy.d.ts
409
402
  /** One piece of policy, by where it sits in the document. */
410
403
  interface PolicyItem {
411
404
  path: string;
@@ -467,5 +460,4 @@ export declare function checkPublication(review: RevisionReview, people: {
467
460
  author: string;
468
461
  approvedBy: readonly string[];
469
462
  }): Result<void, PublicationRefused>;
470
- //#endregion
471
463
  export type { CheckResult, CompileIssue, CompileIssueCode, CompileOptions, CompiledStepSpec, CompiledWorkflow, Decimal, DocumentRole, DocumentStep, Duration, Expr, InvalidWorkflowDocumentError, JsonSchema, KindActor, KindCheck, KindConfig, KindEffect, KindHandlerArgs, KindOutput, PathedIssue, PolicyChange, PolicyItem, PublicationRefused, ResumeDocumentStepError, RevisionReview, WaitingDocumentStep, WorkflowDocument, WorkflowKind };
@@ -1,7 +1,7 @@
1
- import { n as ok, t as err } from "../result-CG_l7xQm.js";
2
- import { b as explicitRetryability, x as isRetryableError } from "../core-COMWflJl.js";
1
+ import { n as ok, t as err } from "../result-CxvNy9B6.js";
2
+ import { C as isRetryableError, S as explicitRetryability, b as continuationOf } from "../core-ut8VZ0sU.js";
3
3
  import { z } from "zod";
4
- //#region src/document/duration.ts
4
+ // src/document/duration.ts
5
5
  function duration(ms) {
6
6
  return { $duration: ms };
7
7
  }
@@ -25,8 +25,7 @@ const DURATION_UNITS = {
25
25
  hours: HOUR,
26
26
  minutes: MINUTE
27
27
  };
28
- //#endregion
29
- //#region src/document/decimal.ts
28
+ // src/document/decimal.ts
30
29
  const DIVISION_SCALE = 9;
31
30
  const PATTERN = /^(-)?(\d+)(?:\.(\d+))?$/;
32
31
  function scaled(d) {
@@ -109,8 +108,7 @@ function decimalCompare(a, b) {
109
108
  const [x, y] = align(scaled(a), scaled(b));
110
109
  return x < y ? -1 : x > y ? 1 : 0;
111
110
  }
112
- //#endregion
113
- //#region src/document/schema.ts
111
+ // src/document/schema.ts
114
112
  const TYPES = /* @__PURE__ */ new Set([
115
113
  "object",
116
114
  "string",
@@ -290,8 +288,7 @@ function assignable(actual, expected) {
290
288
  if (expected.type === "array" && actual.items && expected.items) return assignable(actual.items, expected.items);
291
289
  return true;
292
290
  }
293
- //#endregion
294
- //#region src/document/expr.ts
291
+ // src/document/expr.ts
295
292
  var ExprSyntaxError = class extends Error {};
296
293
  const KEYWORDS = /* @__PURE__ */ new Set([
297
294
  "if",
@@ -853,8 +850,7 @@ function check(e, scope, errors) {
853
850
  }
854
851
  }
855
852
  }
856
- //#endregion
857
- //#region src/document/format.ts
853
+ // src/document/format.ts
858
854
  const WORKFLOW_DOCUMENT_FORMAT = "octaflow.workflow";
859
855
  const WORKFLOW_DOCUMENT_VERSION = 1;
860
856
  const keySchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_-]*$/, "a key starts with a letter and holds letters, digits, \"_\" and \"-\"");
@@ -871,6 +867,7 @@ const documentStepSchema = z.lazy(() => z.strictObject({
871
867
  timeout: z.string().optional(),
872
868
  onTimeout: standIn.optional(),
873
869
  onFailure: standIn.optional(),
870
+ itemOnCancel: z.strictObject({ output: z.record(z.string(), z.unknown()) }).optional(),
874
871
  retry: z.strictObject({
875
872
  maxAttempts: z.number().int().min(1).max(20),
876
873
  backoff: z.enum(["fixed", "exponential"]).optional(),
@@ -903,8 +900,7 @@ const workflowDocumentSchema = z.strictObject({
903
900
  /** Positions and other presentation; the compiler never reads it. */
904
901
  ui: z.record(z.string(), z.unknown()).optional()
905
902
  });
906
- //#endregion
907
- //#region src/document/compile.ts
903
+ // src/document/compile.ts
908
904
  /** The key under which a compiled step's spec rides in its static input. */
909
905
  const SPEC_KEY = "$workflowDocument";
910
906
  /** The step type a kind runs under; a merge (`join: "any"`) is its own type, since the join is the type's. */
@@ -995,6 +991,7 @@ function compileWorkflowDocument(raw, kinds, options = {}) {
995
991
  "timeout",
996
992
  "onTimeout",
997
993
  "onFailure",
994
+ "itemOnCancel",
998
995
  "retry",
999
996
  "join"
1000
997
  ]) if (step[field] !== void 0) issue("shape", `${path}.${field}`, `a flow has no ${field}; set it on the steps inside`);
@@ -1011,6 +1008,7 @@ function compileWorkflowDocument(raw, kinds, options = {}) {
1011
1008
  }
1012
1009
  if (step.steps !== void 0 && step.kind !== "map" && step.kind !== "repeat") issue("shape", `${path}.steps`, "only a flow, a map and a repeat have steps");
1013
1010
  if (step.ref !== void 0) issue("shape", `${path}.ref`, "only a flow refers to another document");
1011
+ if (step.itemOnCancel !== void 0 && step.kind !== "map") issue("shape", `${path}.itemOnCancel`, "only a map has items to withdraw");
1014
1012
  flat.set(key, {
1015
1013
  key,
1016
1014
  path,
@@ -1182,18 +1180,24 @@ function compileWorkflowDocument(raw, kinds, options = {}) {
1182
1180
  refChain: options.refChain
1183
1181
  });
1184
1182
  if (!body) return null;
1183
+ const itemOutput = bodyOutput(body);
1184
+ const itemOnCancel = f.step.itemOnCancel;
1185
+ if (itemOnCancel !== void 0) checkStandIn(itemOnCancel.output, itemOutput, `${f.path}.itemOnCancel.output`, issue);
1185
1186
  return {
1186
1187
  output: {
1187
1188
  type: "object",
1188
1189
  required: ["items"],
1189
1190
  properties: { items: {
1190
1191
  type: "array",
1191
- items: bodyOutput(body)
1192
+ items: itemOutput
1192
1193
  } }
1193
1194
  },
1194
1195
  policy: {
1195
1196
  childType: stepTypeOf("map.item"),
1196
- childInput: { $body: body.definition }
1197
+ childInput: {
1198
+ $body: body.definition,
1199
+ ...itemOnCancel !== void 0 ? { $itemOnCancel: itemOnCancel } : {}
1200
+ }
1197
1201
  }
1198
1202
  };
1199
1203
  }
@@ -1339,7 +1343,7 @@ function compileWorkflowDocument(raw, kinds, options = {}) {
1339
1343
  }
1340
1344
  }
1341
1345
  });
1342
- for (const i of found) issue("params", `${f.path}.params${i.path ? `.${i.path}` : ""}`, i.message);
1346
+ for (const i of found) issue(i.code ?? "params", `${f.path}.params${i.path ? `.${i.path}` : ""}`, i.message);
1343
1347
  }
1344
1348
  for (const g of guardOf.get(key)) {
1345
1349
  const r = checkExpr(g.expr, scope);
@@ -1517,8 +1521,7 @@ function checkStandIn(output, schema, path, issue) {
1517
1521
  const r = zodFor(schema).safeParse(output);
1518
1522
  if (!r.success) for (const i of r.error.issues) issue("schema", `${path}${i.path.length ? `.${i.path.join(".")}` : ""}`, `the stand-in doesn't fit the step's output: ${i.message}`);
1519
1523
  }
1520
- //#endregion
1521
- //#region src/document/kinds.ts
1524
+ // src/document/kinds.ts
1522
1525
  const KIND = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
1523
1526
  function defineKind(config) {
1524
1527
  if (!KIND.test(config.kind)) throw new Error(`Kind "${config.kind}": a kind name is lower case, dotted (mail.send)`);
@@ -1749,8 +1752,7 @@ const coreKinds = [
1749
1752
  subflowKind,
1750
1753
  decideKind
1751
1754
  ];
1752
- //#endregion
1753
- //#region src/document/runtime.ts
1755
+ // src/document/runtime.ts
1754
1756
  const parsedExprs = /* @__PURE__ */ new Map();
1755
1757
  function expr(src) {
1756
1758
  let e = parsedExprs.get(src);
@@ -1821,11 +1823,13 @@ const nonRetryable = (message) => err({
1821
1823
  */
1822
1824
  function thrown(error) {
1823
1825
  const marked = explicitRetryability(error);
1826
+ const continueAfterMs = continuationOf(error);
1824
1827
  return err({
1825
1828
  key: "step_error",
1826
1829
  message: error instanceof Error ? error.message : String(error),
1827
1830
  retryable: marked ?? isRetryableError(error),
1828
- retryableFrom: marked !== void 0 ? "explicit" : "heuristic"
1831
+ retryableFrom: marked !== void 0 ? "explicit" : "heuristic",
1832
+ ...continueAfterMs !== void 0 && { continueAfterMs }
1829
1833
  });
1830
1834
  }
1831
1835
  /**
@@ -1906,7 +1910,10 @@ function registerWorkflowKinds(registry, kinds) {
1906
1910
  index: ctx.stepInput.index,
1907
1911
  with: given
1908
1912
  });
1909
- }, { stepPolicy: (stepInput) => ({ subWorkflowDefinition: stepInput.$body }) });
1913
+ }, { stepPolicy: (stepInput) => ({
1914
+ subWorkflowDefinition: stepInput.$body,
1915
+ ...stepInput.$itemOnCancel !== void 0 ? { onCancelled: stepInput.$itemOnCancel } : {}
1916
+ }) });
1910
1917
  for (const join of ["all", "any"]) registry.register(stepTypeOf(kind.kind, join), handler, {
1911
1918
  condition,
1912
1919
  join,
@@ -2019,8 +2026,7 @@ async function documentResult(store, compiled, workflowId) {
2019
2026
  };
2020
2027
  return Object.fromEntries(Object.entries(compiled.result).map(([name, src]) => [name, evaluate(expr(src), scope)]));
2021
2028
  }
2022
- //#endregion
2023
- //#region src/document/policy.ts
2029
+ // src/document/policy.ts
2024
2030
  /**
2025
2031
  * The policy in a set of steps (a document's, a body's, a case move's):
2026
2032
  *
@@ -2165,5 +2171,4 @@ function checkPublication(review, people) {
2165
2171
  }
2166
2172
  };
2167
2173
  }
2168
- //#endregion
2169
2174
  export { DIVISION_SCALE, ExprSyntaxError, SPEC_KEY, WORKFLOW_DOCUMENT_FORMAT, WORKFLOW_DOCUMENT_VERSION, checkExpr, checkPublication, checkSchemaSubset, compileWorkflowDocument, coreKinds, decideKind, defineKind, describeWaitingStep, diffPolicy, documentResult, documentStepSchema, documentStepSpec, duration, evaluate, eventWaitKind, humanApproveKind, humanTaskKind, isDecimal, isDuration, mapKind, parseExpr, parseIsoDuration, registerWorkflowKinds, repeatKind, resolveParams, resumeDocumentStep, resumeWaitingStep, reviewRevision, reviewWorkflowRevision, roleSchema, sleepKind, stepTypeOf, stepsPolicy, subflowKind, toDecimal, workflowDocumentSchema, workflowPolicy, zodFor };
@@ -1,4 +1,3 @@
1
- //#region src/drizzle/index.d.ts
2
1
  /**
3
2
  * `flow_workflow` — one row per run. `partition_key` is the engine's scoping
4
3
  * column: the store is bound to one partition and every query filters by it.
@@ -169,5 +168,4 @@ export declare const flowStepLeaseColumns: {
169
168
  acquiredAt: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>>;
170
169
  /** A lease past this instant is reaped on the next acquire. */
171
170
  expiresAt: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>;
172
- };
173
- //#endregion
171
+ };
@@ -1,5 +1,5 @@
1
1
  import { bigint, bigserial, doublePrecision, integer, jsonb, text, timestamp } from "drizzle-orm/pg-core";
2
- //#region src/drizzle/index.ts
2
+ // src/drizzle/index.ts
3
3
  /**
4
4
  * `octaflow/drizzle` — the engine's tables as Drizzle column sets.
5
5
  *
@@ -195,5 +195,4 @@ const flowStepLeaseColumns = {
195
195
  /** A lease past this instant is reaped on the next acquire. */
196
196
  expiresAt: timestamptz("expires_at").notNull()
197
197
  };
198
- //#endregion
199
198
  export { flowRateBucketColumns, flowStepEventColumns, flowStepLeaseColumns, flowWorkflowColumns, flowWorkflowStepColumns };
@@ -1,4 +1,4 @@
1
- //#region src/core/store.ts
1
+ // src/core/store.ts
2
2
  /**
3
3
  * The step statuses a step can still be settled from — by `completeStep` or
4
4
  * `failStep`. Every other status is final for those two writes: a step that
@@ -10,8 +10,7 @@ const UNSETTLED_STEP_STATUSES = [
10
10
  "waiting",
11
11
  "mapping"
12
12
  ];
13
- //#endregion
14
- //#region src/core/gate.ts
13
+ // src/core/gate.ts
15
14
  /**
16
15
  * The rule keys a request is held to, in the order they are checked: the
17
16
  * step's type, then each resource it declares, without repeats. The shipped
@@ -118,5 +117,4 @@ function createInMemoryStepGate(config = {}) {
118
117
  };
119
118
  } };
120
119
  }
121
- //#endregion
122
120
  export { gateRuleKeys as n, UNSETTLED_STEP_STATUSES as r, createInMemoryStepGate as t };
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- //#region src/core/result.d.ts
3
2
  interface FlowErrorShape {
4
3
  key: string;
5
4
  message: string;
@@ -13,8 +12,6 @@ type Result<T = void, E extends FlowErrorShape = FlowErrorShape> = {
13
12
  };
14
13
  declare const ok: <T>(value: T) => Result<T, never>;
15
14
  declare const err: <E extends FlowErrorShape>(error: E) => Result<never, E>;
16
- //#endregion
17
- //#region src/core/readiness.d.ts
18
15
  /**
19
16
  * How a step's dependencies gate it.
20
17
  *
@@ -55,8 +52,6 @@ interface ReadinessPlan {
55
52
  * @param joinOf the join rule for a step type, from its registration
56
53
  */
57
54
  declare function computeReadiness(steps: StepRecord[], joinOf: (stepType: string) => JoinRule): ReadinessPlan;
58
- //#endregion
59
- //#region src/core/types.d.ts
60
55
  /** Workflow / step identifiers. Numeric to match common bigserial-backed stores. */
61
56
  type WorkflowId = number;
62
57
  type StepId = number;
@@ -163,9 +158,23 @@ interface StepExecutionContext<TContext = unknown> {
163
158
  * `true`.
164
159
  */
165
160
  heartbeat: () => Promise<boolean>;
161
+ /**
162
+ * What an earlier attempt of this step kept (`value`, null on the first), and
163
+ * how this attempt keeps its own (`save`, which replaces it). For a handler
164
+ * that does several things that cost or change something — an agent's tool
165
+ * calls — so a retry after a crash picks up where the last attempt got to.
166
+ * `save` resolves `false` when nothing was written: the step is no longer this
167
+ * attempt's, or the store keeps no checkpoints.
168
+ */
169
+ checkpoint?: StepCheckpoint;
166
170
  /** Host-provided per-step context. */
167
171
  context: TContext;
168
172
  }
173
+ /** See {@link StepExecutionContext.checkpoint}. */
174
+ interface StepCheckpoint {
175
+ readonly value: unknown;
176
+ save(value: unknown): Promise<boolean>;
177
+ }
169
178
  interface StepError extends FlowErrorShape {
170
179
  key: 'step_error';
171
180
  message: string;
@@ -177,6 +186,8 @@ interface StepError extends FlowErrorShape {
177
186
  * the engine's `defaultRetryable` may override those, and only those.
178
187
  */
179
188
  retryableFrom?: 'explicit' | 'predicate' | 'heuristic';
189
+ /** The handler threw `continueStep`: not a failure — run the step again after this many ms, without spending an attempt. */
190
+ continueAfterMs?: number;
180
191
  }
181
192
  type StepHandler<TContext = unknown> = (ctx: StepExecutionContext<TContext>) => Promise<Result<Record<string, unknown>, StepError>>;
182
193
  /** Context for a step's compensation handler — the execution context plus the step's own output. */
@@ -350,6 +361,15 @@ interface StepPolicy {
350
361
  waitTimeoutMs?: number;
351
362
  onTimeout?: WaitTimeoutPolicy;
352
363
  onFailure?: StepFailurePolicy;
364
+ /**
365
+ * For a step that runs a sub-workflow (a map item among them): what it completes with when that run is
366
+ * cancelled — `cancelWorkflow` on the child — instead of failing. The step keeps "Sub-workflow cancelled"
367
+ * on its `error`, and a map counts the stand-in in the item's slot. A child that *fails* still fails the
368
+ * step as `onFailure` says.
369
+ */
370
+ onCancelled?: {
371
+ output: Record<string, unknown>;
372
+ };
353
373
  retry?: RetryPolicy;
354
374
  /** This step starts a child workflow from this definition: the handler returns its input. */
355
375
  subWorkflowDefinition?: WorkflowDefinition;
@@ -478,8 +498,6 @@ interface WorkflowStepRunningError extends FlowErrorShape {
478
498
  * reject a start with a domain-specific error key the engine just passes through.
479
499
  */
480
500
  type FlowError = WorkflowNotFoundError | InvalidWorkflowDefinitionError | StepHandlerNotFoundError | WorkflowNotRetryableError | WorkflowStepRunningError | StepError | FlowErrorShape;
481
- //#endregion
482
- //#region src/core/defineStep.d.ts
483
501
  /**
484
502
  * A type-safe step definition with Zod validation schemas, parameterized by the
485
503
  * host `TContext`. Created by `defineStep()`, consumed by `buildWorkflow()`.
@@ -565,6 +583,8 @@ interface TypedStepContext<TInput extends Record<string, unknown>, TDeps extends
565
583
  * re-queued as dead). See `heartbeatTimeoutMs`.
566
584
  */
567
585
  heartbeat: () => Promise<boolean>;
586
+ /** What an earlier attempt kept, and how this one keeps its own — see `StepExecutionContext.checkpoint`. */
587
+ checkpoint?: StepCheckpoint;
568
588
  context: TContext;
569
589
  }
570
590
  interface DefineStepConfig<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>, TDeps extends Record<string, TypedStep<any, any, any, TContext>>, TContext, TJoin extends JoinRule = 'all'> {
@@ -861,8 +881,6 @@ interface BuildWorkflowConfig<TInput extends Record<string, unknown>, TSteps ext
861
881
  * that every dependency key references a real step.
862
882
  */
863
883
  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>;
864
- //#endregion
865
- //#region src/core/decision.d.ts
866
884
  /** The questions a decision answers: each key names a question, each value its options. */
867
885
  type DecisionQuestions = Readonly<Record<string, readonly [string, ...string[]]>>;
868
886
  /** One answer: the chosen option, how sure the decider is, and optionally why. */
@@ -1037,8 +1055,6 @@ declare function defineDecisionOutcomeStep<Q extends DecisionQuestions, TDecisio
1037
1055
  /** Its review, keyed by its step key: `{ triageReview }`. */
1038
1056
  review: { [K in TReviewKey]: TypedStep<any, DecisionReview<Q>, any, TContext>; };
1039
1057
  }): TypedStep<Record<string, never>, DecisionOutcome<Q>, { [K in TDecisionKey]: DecisionStep<Q, any, any, TContext>; } & { [K in TReviewKey]: TypedStep<any, DecisionReview<Q>, any, TContext>; }, TContext>;
1040
- //#endregion
1041
- //#region src/core/public-view.d.ts
1042
1058
  /** The five states a UI renders. Suspensions display as in-flight. */
1043
1059
  type StepDisplayStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
1044
1060
  declare const STEP_DISPLAY_STATUS_SCHEMA: z.ZodEnum<{
@@ -1165,5 +1181,4 @@ declare function toPublicStep(step: StepRecord): PublicWorkflowStep;
1165
1181
  * `idempotencyKey`, `parentWorkflowId`/`parentStepId`, and `metadata`.
1166
1182
  */
1167
1183
  declare function toPublicWorkflow(workflow: WorkflowWithSteps): PublicWorkflow;
1168
- //#endregion
1169
- export { StepFailurePolicy as $, StepOutput as A, defineWaitStep as B, defineDecisionOutcomeStep as C, isTerminalStepStatus as Ct, resolveDecision as D, ok as Dt, functionDecider as E, err as Et, buildWorkflow as F, StartOptions as G, FlowError as H, defineMapStep as I, StepConditionHandler as J, StepCompensateHandler as K, defineSleepStep as L, TypedStepContext as M, TypedWorkflow as N, DefineStepConfig as O, WorkflowOutput as P, StepExecutionContext as Q, defineStep as R, DecisionStep as S, computeReadiness as St, defineDecisionStep as T, Result as Tt, InvalidWorkflowDefinitionError as U, DelayedStep as V, RetryPolicy as W, StepDelay as X, StepDefinition as Y, StepError as Z, DecisionOutput as _, WorkflowStepRunningError as _t, STEP_DISPLAY_STATUS as a, StepRecord as at, DecisionResponse as b, PlannedSkip as bt, WORKFLOW_STATUS_SCHEMA as c, StepStatus as ct, toPublicWorkflow as d, WorkflowDefinition as dt, StepHandler as et, Decider as f, WorkflowId as ft, DecisionOutcome as g, WorkflowStatus as gt, DecisionChoices as h, WorkflowRecord as ht, PublicWorkflowStep as i, StepPolicy as it, TypedStep as j, StepDeps as k, toDisplayStepStatus as l, WaitTimeoutPolicy as lt, DecisionAnswers as m, WorkflowNotRetryableError as mt, PUBLIC_WORKFLOW_STEP_SCHEMA as n, StepHandlerRegistry as nt, STEP_DISPLAY_STATUS_SCHEMA as o, StepRegistration as ot, DecisionAnswer as p, WorkflowNotFoundError as pt, StepCompensationContext as q, PublicWorkflow as r, StepId as rt, StepDisplayStatus as s, StepRepeat as st, PUBLIC_WORKFLOW_SCHEMA as t, StepHandlerNotFoundError as tt, toPublicStep as u, WorkflowCreatedResult as ut, DecisionQuestions as v, WorkflowWithSteps as vt, defineDecisionReviewStep as w, FlowErrorShape as wt, DecisionReview as x, ReadinessPlan as xt, DecisionRequest as y, JoinRule as yt, defineSubWorkflowStep as z };
1184
+ export { StepExecutionContext as $, StepOutput as A, defineWaitStep as B, defineDecisionOutcomeStep as C, computeReadiness as Ct, resolveDecision as D, err as Dt, functionDecider as E, Result as Et, buildWorkflow as F, StartOptions as G, FlowError as H, defineMapStep as I, StepCompensationContext as J, StepCheckpoint as K, defineSleepStep as L, TypedStepContext as M, TypedWorkflow as N, DefineStepConfig as O, ok as Ot, WorkflowOutput as P, StepError as Q, defineStep as R, DecisionStep as S, ReadinessPlan as St, defineDecisionStep as T, FlowErrorShape as Tt, InvalidWorkflowDefinitionError as U, DelayedStep as V, RetryPolicy as W, StepDefinition as X, StepConditionHandler as Y, StepDelay as Z, DecisionOutput as _, WorkflowStatus as _t, STEP_DISPLAY_STATUS as a, StepPolicy as at, DecisionResponse as b, JoinRule as bt, WORKFLOW_STATUS_SCHEMA as c, StepRepeat as ct, toPublicWorkflow as d, WorkflowCreatedResult as dt, StepFailurePolicy as et, Decider as f, WorkflowDefinition as ft, DecisionOutcome as g, WorkflowRecord as gt, DecisionChoices as h, WorkflowNotRetryableError as ht, PublicWorkflowStep as i, StepId as it, TypedStep as j, StepDeps as k, toDisplayStepStatus as l, StepStatus as lt, DecisionAnswers as m, WorkflowNotFoundError as mt, PUBLIC_WORKFLOW_STEP_SCHEMA as n, StepHandlerNotFoundError as nt, STEP_DISPLAY_STATUS_SCHEMA as o, StepRecord as ot, DecisionAnswer as p, WorkflowId as pt, StepCompensateHandler as q, PublicWorkflow as r, StepHandlerRegistry as rt, StepDisplayStatus as s, StepRegistration as st, PUBLIC_WORKFLOW_SCHEMA as t, StepHandler as tt, toPublicStep as u, WaitTimeoutPolicy as ut, DecisionQuestions as v, WorkflowStepRunningError as vt, defineDecisionReviewStep as w, isTerminalStepStatus as wt, DecisionReview as x, PlannedSkip as xt, DecisionRequest as y, WorkflowWithSteps as yt, defineSubWorkflowStep as z };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { $ as StepFailurePolicy, A as StepOutput, B as defineWaitStep, C as defineDecisionOutcomeStep, Ct as isTerminalStepStatus, D as resolveDecision, Dt as ok, E as functionDecider, Et as err, F as buildWorkflow, G as StartOptions, H as FlowError, I as defineMapStep, J as StepConditionHandler, K as StepCompensateHandler, L as defineSleepStep, M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, P as WorkflowOutput, Q as StepExecutionContext, R as defineStep, S as DecisionStep, St as computeReadiness, T as defineDecisionStep, Tt as Result, U as InvalidWorkflowDefinitionError, V as DelayedStep, W as RetryPolicy, X as StepDelay, Y as StepDefinition, Z as StepError, _ as DecisionOutput, _t as WorkflowStepRunningError, a as STEP_DISPLAY_STATUS, at as StepRecord, b as DecisionResponse, bt as PlannedSkip, c as WORKFLOW_STATUS_SCHEMA, ct as StepStatus, d as toPublicWorkflow, dt as WorkflowDefinition, et as StepHandler, f as Decider, ft as WorkflowId, g as DecisionOutcome, gt as WorkflowStatus, h as DecisionChoices, ht as WorkflowRecord, i as PublicWorkflowStep, it as StepPolicy, j as TypedStep, k as StepDeps, l as toDisplayStepStatus, lt as WaitTimeoutPolicy, m as DecisionAnswers, mt as WorkflowNotRetryableError, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepHandlerRegistry, o as STEP_DISPLAY_STATUS_SCHEMA, ot as StepRegistration, p as DecisionAnswer, pt as WorkflowNotFoundError, q as StepCompensationContext, r as PublicWorkflow, rt as StepId, s as StepDisplayStatus, st as StepRepeat, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepHandlerNotFoundError, u as toPublicStep, ut as WorkflowCreatedResult, v as DecisionQuestions, vt as WorkflowWithSteps, w as defineDecisionReviewStep, wt as FlowErrorShape, x as DecisionReview, xt as ReadinessPlan, y as DecisionRequest, yt as JoinRule, z as defineSubWorkflowStep } from "./index-BlwtmRJP.js";
2
- import { a as LogAttributes, i as EnqueueOptions, n as DispatchStepPayload, o as Logger, r as Dispatcher, s as noopLogger, t as DispatchKind } from "./dispatcher-jbFVHfdm.js";
3
- import { a as CreatedWorkflow, c as ListWorkflowsFilters, d as UNSETTLED_STEP_STATUSES, f as WorkflowStore, i as CreateWorkflowStep, l as ReopenWorkflowParams, n as CompleteStepParams, o as FailStepParams, p as WorkflowTreeFilter, r as CreateWorkflowParams, s as FinishWorkflowParams, t as AddChildStep, u as TransactionalScope } from "./store-BPHVI_a3.js";
4
- import { C as gateRuleKeys, S as createInMemoryStepGate, _ 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, 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, x as StepGateRequest, y as StepGate } from "./observability-BgUtESlj.js";
5
- //#region src/core/hooks.d.ts
1
+ import { $ as StepExecutionContext, A as StepOutput, B as defineWaitStep, C as defineDecisionOutcomeStep, Ct as computeReadiness, D as resolveDecision, Dt as err, E as functionDecider, Et as Result, F as buildWorkflow, G as StartOptions, H as FlowError, I as defineMapStep, J as StepCompensationContext, K as StepCheckpoint, L as defineSleepStep, M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, Ot as ok, P as WorkflowOutput, Q as StepError, R as defineStep, S as DecisionStep, St as ReadinessPlan, T as defineDecisionStep, Tt as FlowErrorShape, U as InvalidWorkflowDefinitionError, V as DelayedStep, W as RetryPolicy, X as StepDefinition, Y as StepConditionHandler, Z as StepDelay, _ as DecisionOutput, _t as WorkflowStatus, a as STEP_DISPLAY_STATUS, at as StepPolicy, b as DecisionResponse, bt as JoinRule, c as WORKFLOW_STATUS_SCHEMA, ct as StepRepeat, d as toPublicWorkflow, dt as WorkflowCreatedResult, et as StepFailurePolicy, f as Decider, ft as WorkflowDefinition, g as DecisionOutcome, gt as WorkflowRecord, h as DecisionChoices, ht as WorkflowNotRetryableError, i as PublicWorkflowStep, it as StepId, j as TypedStep, k as StepDeps, l as toDisplayStepStatus, lt as StepStatus, m as DecisionAnswers, mt as WorkflowNotFoundError, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepHandlerNotFoundError, o as STEP_DISPLAY_STATUS_SCHEMA, ot as StepRecord, p as DecisionAnswer, pt as WorkflowId, q as StepCompensateHandler, r as PublicWorkflow, rt as StepHandlerRegistry, s as StepDisplayStatus, st as StepRegistration, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepHandler, u as toPublicStep, ut as WaitTimeoutPolicy, v as DecisionQuestions, vt as WorkflowStepRunningError, w as defineDecisionReviewStep, wt as isTerminalStepStatus, x as DecisionReview, xt as PlannedSkip, y as DecisionRequest, yt as WorkflowWithSteps, z as defineSubWorkflowStep } from "./index-BBO5BGS8.js";
2
+ import { a as LogAttributes, i as EnqueueOptions, n as DispatchStepPayload, o as Logger, r as Dispatcher, s as noopLogger, t as DispatchKind } from "./dispatcher-BnlAJuu3.js";
3
+ import { a as CreatedWorkflow, c as ListWorkflowsFilters, d as UNSETTLED_STEP_STATUSES, f as WorkflowStore, i as CreateWorkflowStep, l as ReopenWorkflowParams, n as CompleteStepParams, o as FailStepParams, p as WorkflowTreeFilter, r as CreateWorkflowParams, s as FinishWorkflowParams, t as AddChildStep, u as TransactionalScope } from "./store-oCpqAuTT.js";
4
+ import { C as gateRuleKeys, S as createInMemoryStepGate, _ 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, 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, x as StepGateRequest, y as StepGate } from "./observability-C-wtcDDo.js";
6
5
  export interface BeforeStartArgs {
7
6
  definition: WorkflowDefinition;
8
7
  input: Record<string, unknown>;
@@ -55,16 +54,12 @@ export interface WorkflowHooks<TContext = unknown> {
55
54
  /** Runs once when a workflow reaches `completed` (e.g. daily usage rollup). */
56
55
  onWorkflowCompleted?(args: WorkflowCompletedArgs): Promise<void>;
57
56
  }
58
- //#endregion
59
- //#region src/core/registry.d.ts
60
57
  /**
61
58
  * Creates an in-memory step handler registry.
62
59
  * Populate it at application startup (e.g. by registering every workflow's steps).
63
60
  * Each registration carries the handler plus its optional retry/timeout policy.
64
61
  */
65
62
  export declare function createStepHandlerRegistry<TContext = unknown>(): StepHandlerRegistry<TContext>;
66
- //#endregion
67
- //#region src/core/retry.d.ts
68
63
  /**
69
64
  * Tag an error with an explicit retryability decision, overriding both the
70
65
  * step predicate and the message heuristic. Returns the same error, so it can
@@ -105,8 +100,17 @@ export declare function explicitRetryability(error: unknown): boolean | undefine
105
100
  * `defaultRetryable` when the answer matters.
106
101
  */
107
102
  export declare function isRetryableError(error: unknown): boolean;
108
- //#endregion
109
- //#region src/core/engine.d.ts
103
+ /**
104
+ * Throw this from a handler to stop now and run again later, without spending
105
+ * an attempt: the engine puts the step back to `pending` and re-queues it
106
+ * after `afterMs`. For a handler that works in slices of wall-clock time — a
107
+ * host with a hard request ceiling — and keeps its progress in
108
+ * `ctx.checkpoint`, so the next run goes on from there. Nothing caps how
109
+ * often a step continues; the handler's own limits must.
110
+ */
111
+ export declare function continueStep(afterMs?: number, message?: string): Error;
112
+ /** The delay a {@link continueStep} error asks for, or `undefined` for any other error. */
113
+ export declare function continuationOf(error: unknown): number | undefined;
110
114
  export interface WorkflowEngineConfig {
111
115
  /** The dispatcher's per-step expiry, in seconds. Used by the stuck-step sweeper. Default 600. */
112
116
  stepExpirySeconds?: number;
@@ -205,8 +209,6 @@ export declare function createWorkflowEngine<TContext = unknown>(deps: WorkflowE
205
209
  }) => Promise<Result<WorkflowWithSteps[], FlowError>>;
206
210
  };
207
211
  export type WorkflowEngine<TContext = unknown> = ReturnType<typeof createWorkflowEngine<TContext>>;
208
- //#endregion
209
- //#region src/core/in-memory-store.d.ts
210
212
  /**
211
213
  * Reference `WorkflowStore` backed by plain Maps. Intended for tests and single-
212
214
  * process experimentation — NOT durable. Partition-scoped at construction to
@@ -220,5 +222,4 @@ export declare function createInMemoryWorkflowStore(partitionKey?: string): Work
220
222
  steps: StepRecord[];
221
223
  };
222
224
  };
223
- //#endregion
224
- export { AddChildStep, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, Decider, DecisionAnswer, DecisionAnswers, DecisionChoices, DecisionOutcome, DecisionOutput, DecisionQuestions, DecisionRequest, DecisionResponse, DecisionReview, DecisionStep, DefineStepConfig, DelayedStep, 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, StepDelay, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepFailurePolicy, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepPolicy, StepRecord, StepRegistration, StepRepeat, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, UNSETTLED_STEP_STATUSES, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCreatedResult, WorkflowDefinition, 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, gateRuleKeys, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, resolveDecision, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
225
+ export { AddChildStep, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, Decider, DecisionAnswer, DecisionAnswers, DecisionChoices, DecisionOutcome, DecisionOutput, DecisionQuestions, DecisionRequest, DecisionResponse, DecisionReview, DecisionStep, DefineStepConfig, DelayedStep, 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, StepCheckpoint, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDelay, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepFailurePolicy, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepPolicy, StepRecord, StepRegistration, StepRepeat, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, UNSETTLED_STEP_STATUSES, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCreatedResult, WorkflowDefinition, 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, gateRuleKeys, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, resolveDecision, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
package/dist/index.js CHANGED
@@ -1,14 +1,13 @@
1
- import { n as ok, t as err } from "./result-CG_l7xQm.js";
2
- import { n as gateRuleKeys, r as UNSETTLED_STEP_STATUSES, t as createInMemoryStepGate } from "./gate-BBZK5OgY.js";
3
- import { C as nonRetryableError, S as markRetryable, _ 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-COMWflJl.js";
4
- //#region src/core/logger.ts
1
+ import { n as ok, t as err } from "./result-CxvNy9B6.js";
2
+ import { n as gateRuleKeys, r as UNSETTLED_STEP_STATUSES, t as createInMemoryStepGate } from "./gate-BcjK7FoL.js";
3
+ import { C as isRetryableError, E as retryableError, S as explicitRetryability, T as nonRetryableError, _ as defineStep, a as WORKFLOW_STATUS_SCHEMA, b as continuationOf, 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 markRetryable, x as continueStep, y as defineWaitStep } from "./core-ut8VZ0sU.js";
4
+ // src/core/logger.ts
5
5
  const noopLogger = {
6
6
  info() {},
7
7
  warn() {},
8
8
  error() {}
9
9
  };
10
- //#endregion
11
- //#region src/core/readiness.ts
10
+ // src/core/readiness.ts
12
11
  /** Statuses a step can no longer move out of on its own. */
13
12
  const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
14
13
  "completed",
@@ -74,8 +73,7 @@ function computeReadiness(steps, joinOf) {
74
73
  skip
75
74
  };
76
75
  }
77
- //#endregion
78
- //#region src/core/observability.ts
76
+ // src/core/observability.ts
79
77
  const noopObserver = { record() {} };
80
78
  function createRecordingObserver() {
81
79
  const events = [];
@@ -199,8 +197,7 @@ function createRecordingTracer() {
199
197
  }
200
198
  };
201
199
  }
202
- //#endregion
203
- //#region src/core/registry.ts
200
+ // src/core/registry.ts
204
201
  /**
205
202
  * Creates an in-memory step handler registry.
206
203
  * Populate it at application startup (e.g. by registering every workflow's steps).
@@ -244,8 +241,7 @@ function createStepHandlerRegistry() {
244
241
  }
245
242
  };
246
243
  }
247
- //#endregion
248
- //#region src/core/engine.ts
244
+ // src/core/engine.ts
249
245
  /**
250
246
  * Run a handler under an optional wall-clock timeout. On expiry the abort signal
251
247
  * is fired (cooperative handlers stop) and a retryable timeout error is returned —
@@ -476,6 +472,7 @@ function createWorkflowEngine(deps) {
476
472
  waitTimeoutMs: own.waitTimeoutMs ?? registration?.timeoutMs,
477
473
  onTimeout: own.onTimeout ?? registration?.onTimeout ?? "fail",
478
474
  onFailure: own.onFailure ?? registration?.onFailure ?? "fail",
475
+ onCancelled: own.onCancelled,
479
476
  retry: own.retry ?? registration?.retry,
480
477
  subWorkflowDefinition: own.subWorkflowDefinition ?? registration?.subWorkflowDefinition,
481
478
  childType: own.childType ?? registration?.childType,
@@ -765,6 +762,39 @@ function createWorkflowEngine(deps) {
765
762
  function start(workflow, input, options) {
766
763
  return startWorkflow(workflow.definition, workflow.inputSchema.parse(input), options);
767
764
  }
765
+ /**
766
+ * A handler yielded with `continueStep`: hand the step back without spending
767
+ * the attempt and run it again after `afterMs`. The release is fenced by the
768
+ * claim, so a superseded invocation re-queues nothing.
769
+ */
770
+ async function continueLater(workflow, step, attemptNo, afterMs) {
771
+ const workflowId = workflow.id;
772
+ if (!await store.releaseStep(step.id, attemptNo)) {
773
+ logger.warn("Discarding the continuation of a superseded step", {
774
+ workflowId,
775
+ stepId: step.id,
776
+ stepKey: step.key,
777
+ attempt: attemptNo
778
+ });
779
+ return;
780
+ }
781
+ logger.info("Step continues later", {
782
+ workflowId,
783
+ stepId: step.id,
784
+ stepKey: step.key,
785
+ afterMs
786
+ });
787
+ const re = await dispatcher.enqueueStep({
788
+ workflowId,
789
+ stepId: step.id,
790
+ stepKey: step.key,
791
+ stepType: step.type
792
+ }, { startAfterSeconds: Math.ceil(afterMs / 1e3) });
793
+ if (!re.ok) await failOrDefault(workflowId, step, `the step could not be queued to continue: ${re.error.message}`, {
794
+ workflowType: workflow.type,
795
+ attempt: attemptNo
796
+ });
797
+ }
768
798
  async function executeStep(workflowId, stepId) {
769
799
  const workflow = await store.getWorkflow(workflowId);
770
800
  if (!workflow) return {
@@ -956,6 +986,10 @@ function createWorkflowEngine(deps) {
956
986
  dependencyOutputs,
957
987
  signal: abort.signal,
958
988
  heartbeat: heartbeat.beat,
989
+ checkpoint: {
990
+ value: step.metadata?.checkpoint ?? null,
991
+ save: (value) => store.saveStepCheckpoint ? store.saveStepCheckpoint(stepId, attemptNo, value) : Promise.resolve(false)
992
+ },
959
993
  context
960
994
  };
961
995
  const guard = registration.condition ? await registration.condition(ctx) : void 0;
@@ -1063,6 +1097,13 @@ function createWorkflowEngine(deps) {
1063
1097
  value: void 0
1064
1098
  };
1065
1099
  }
1100
+ if (handlerResult.error.continueAfterMs !== void 0) {
1101
+ await continueLater(workflow, step, attemptNo, handlerResult.error.continueAfterMs);
1102
+ return {
1103
+ ok: true,
1104
+ value: void 0
1105
+ };
1106
+ }
1066
1107
  if ((handlerResult.error.retryableFrom === "heuristic" && defaultRetryable !== void 0 ? defaultRetryable : handlerResult.error.retryable) && attemptNo < maxAttempts) {
1067
1108
  const delaySeconds = backoffDelaySeconds(retry, attemptNo);
1068
1109
  logger.info("Retrying step", {
@@ -1115,6 +1156,14 @@ function createWorkflowEngine(deps) {
1115
1156
  value: void 0
1116
1157
  };
1117
1158
  } catch (error) {
1159
+ const afterMs = continuationOf(error);
1160
+ if (afterMs !== void 0 && !heartbeat.lost) {
1161
+ await continueLater(workflow, step, attemptNo, afterMs);
1162
+ return {
1163
+ ok: true,
1164
+ value: void 0
1165
+ };
1166
+ }
1118
1167
  const message = error instanceof Error ? error.message : "Unknown step execution error";
1119
1168
  logger.error("Step execution threw", error instanceof Error ? error : new Error(message), {
1120
1169
  workflowId,
@@ -1531,6 +1580,25 @@ function createWorkflowEngine(deps) {
1531
1580
  else await onStepCompleted(child.parentWorkflowId, child.parentStepId, done);
1532
1581
  } else if (parentStep.parentStepId != null) await onChildCompleted(child.parentWorkflowId, parentStep, output);
1533
1582
  else await onStepCompleted(child.parentWorkflowId, child.parentStepId, output);
1583
+ } else if (child.status === "cancelled" && policyOf(parentStep).onCancelled) {
1584
+ const message = "Sub-workflow cancelled";
1585
+ const standIn = policyOf(parentStep).onCancelled.output;
1586
+ const announce = () => emit({
1587
+ type: "step.defaulted",
1588
+ workflowId: child.parentWorkflowId,
1589
+ stepId: parentStep.id,
1590
+ stepKey: parentStep.key,
1591
+ stepType: parentStep.type,
1592
+ error: message
1593
+ });
1594
+ if (parentStep.parentStepId != null) await onChildCompleted(child.parentWorkflowId, parentStep, standIn, {
1595
+ error: message,
1596
+ announce
1597
+ });
1598
+ else await onStepCompleted(child.parentWorkflowId, child.parentStepId, standIn, {
1599
+ error: message,
1600
+ announce
1601
+ });
1534
1602
  } else await failOrDefault(child.parentWorkflowId, parentStep, `Sub-workflow ${child.status}${child.error ? `: ${child.error}` : ""}`);
1535
1603
  logger.info("Bridged sub-workflow to parent step", {
1536
1604
  childWorkflowId,
@@ -2092,8 +2160,7 @@ function createWorkflowEngine(deps) {
2092
2160
  listWorkflows
2093
2161
  };
2094
2162
  }
2095
- //#endregion
2096
- //#region src/core/in-memory-store.ts
2163
+ // src/core/in-memory-store.ts
2097
2164
  /**
2098
2165
  * Reference `WorkflowStore` backed by plain Maps. Intended for tests and single-
2099
2166
  * process experimentation — NOT durable. Partition-scoped at construction to
@@ -2190,6 +2257,15 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
2190
2257
  s.attempts += 1;
2191
2258
  return true;
2192
2259
  }
2260
+ async function saveStepCheckpoint(stepId, attempt, value) {
2261
+ const s = steps.get(stepId);
2262
+ if (!s || s.status !== "running" || s.attempts !== attempt) return false;
2263
+ s.metadata = {
2264
+ ...s.metadata ?? {},
2265
+ checkpoint: clone(value)
2266
+ };
2267
+ return true;
2268
+ }
2193
2269
  async function heartbeatStep(stepId, at) {
2194
2270
  const s = steps.get(stepId);
2195
2271
  if (!s || s.status !== "running") return false;
@@ -2198,6 +2274,14 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
2198
2274
  s.heartbeatAt = at;
2199
2275
  return true;
2200
2276
  }
2277
+ async function releaseStep(stepId, attempt) {
2278
+ const s = steps.get(stepId);
2279
+ if (!s || s.status !== "running" || s.attempts !== attempt) return false;
2280
+ s.status = "pending";
2281
+ s.attempts -= 1;
2282
+ s.heartbeatAt = null;
2283
+ return true;
2284
+ }
2201
2285
  async function markStepPending(stepId) {
2202
2286
  const s = steps.get(stepId);
2203
2287
  if (s) s.status = "pending";
@@ -2386,7 +2470,9 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
2386
2470
  listSteps,
2387
2471
  markStepRunning,
2388
2472
  heartbeatStep,
2473
+ saveStepCheckpoint,
2389
2474
  markStepPending,
2475
+ releaseStep,
2390
2476
  markStepWaiting,
2391
2477
  markStepMapping,
2392
2478
  markStepCompensating,
@@ -2414,5 +2500,4 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
2414
2500
  }
2415
2501
  };
2416
2502
  }
2417
- //#endregion
2418
- export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, UNSETTLED_STEP_STATUSES, WORKFLOW_STATUS_SCHEMA, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createInMemoryWorkflowStore, createMeterObserver, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineDecisionOutcomeStep, defineDecisionReviewStep, defineDecisionStep, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, functionDecider, gateRuleKeys, isRetryableError, isTerminalStepStatus, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, resolveDecision, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
2503
+ export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, UNSETTLED_STEP_STATUSES, WORKFLOW_STATUS_SCHEMA, buildWorkflow, combineObservers, computeReadiness, continuationOf, continueStep, createInMemoryStepGate, createInMemoryWorkflowStore, createMeterObserver, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineDecisionOutcomeStep, defineDecisionReviewStep, defineDecisionStep, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, functionDecider, gateRuleKeys, isRetryableError, isTerminalStepStatus, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, resolveDecision, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
@@ -1,5 +1,4 @@
1
- import { ft as WorkflowId, rt as StepId } from "./index-BlwtmRJP.js";
2
- //#region src/core/gate.d.ts
1
+ import { it as StepId, pt as WorkflowId } from "./index-BBO5BGS8.js";
3
2
  interface StepGateRequest {
4
3
  partitionKey: string;
5
4
  workflowId: WorkflowId;
@@ -77,8 +76,6 @@ interface InMemoryStepGateConfig {
77
76
  * for multi-worker concurrency control.
78
77
  */
79
78
  declare function createInMemoryStepGate(config?: InMemoryStepGateConfig): StepGate;
80
- //#endregion
81
- //#region src/core/observability.d.ts
82
79
  /** Lifecycle event types the engine emits at every workflow/step transition. */
83
80
  type FlowEventType = 'workflow.started' | 'workflow.completed' | 'workflow.failed' | 'workflow.cancelled' |
84
81
  /** A `failed` workflow was put back in flight by `retryWorkflow`. */
@@ -199,5 +196,4 @@ interface RecordingTracer extends FlowTracer {
199
196
  readonly spans: RecordedSpan[];
200
197
  }
201
198
  declare function createRecordingTracer(): RecordingTracer;
202
- //#endregion
203
199
  export { gateRuleKeys as C, createInMemoryStepGate as S, 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, 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, StepGateRequest as x, StepGate as y };
@@ -1,8 +1,7 @@
1
- import "../index-BlwtmRJP.js";
2
- import { o as Logger, r as Dispatcher } from "../dispatcher-jbFVHfdm.js";
1
+ import "../index-BBO5BGS8.js";
2
+ import { o as Logger, r as Dispatcher } from "../dispatcher-BnlAJuu3.js";
3
3
  import { z } from "zod";
4
4
  import { PgBoss, Schedule } from "pg-boss";
5
- //#region src/pg-boss/payload.d.ts
6
5
  /**
7
6
  * The wire payload for a step job. It is the flow-core `DispatchStepPayload`
8
7
  * plus the `partitionKey` — the worker needs the partition to reconstruct a
@@ -64,8 +63,6 @@ export interface StartJobContext extends WireStartPayload {
64
63
  }
65
64
  /** Resolve the per-delivery idempotency key. Explicit key wins over a prefix. */
66
65
  export declare function resolveStartIdempotencyKey(payload: WireStartPayload, jobId: string): string | undefined;
67
- //#endregion
68
- //#region src/pg-boss/queue.d.ts
69
66
  /**
70
67
  * Create the step queue and its dead-letter queue if they do not exist. Call
71
68
  * once at startup; the dispatcher also runs it from `prepare` and lazily on a
@@ -146,8 +143,6 @@ export declare function createPgBossDlqWorker(deps: PgBossDlqWorkerDeps): {
146
143
  start: (onDlq: DlqProcessor) => Promise<void>;
147
144
  stop: () => Promise<void>;
148
145
  };
149
- //#endregion
150
- //#region src/pg-boss/scheduler.d.ts
151
146
  /** Create the start queue (idempotent). */
152
147
  export declare function ensureStartQueue(boss: PgBoss, queueName: string): Promise<void>;
153
148
  export interface PgBossSchedulerDeps {
@@ -221,5 +216,4 @@ export interface PgBossStartWorkerDeps {
221
216
  export declare function createPgBossStartWorker(deps: PgBossStartWorkerDeps): {
222
217
  start: (process: StartJobProcessor) => Promise<void>;
223
218
  stop: () => Promise<void>;
224
- };
225
- //#endregion
219
+ };
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- //#region src/pg-boss/payload.ts
2
+ // src/pg-boss/payload.ts
3
3
  /**
4
4
  * The wire payload for a step job. It is the flow-core `DispatchStepPayload`
5
5
  * plus the `partitionKey` — the worker needs the partition to reconstruct a
@@ -51,8 +51,7 @@ function resolveStartIdempotencyKey(payload, jobId) {
51
51
  if (payload.idempotencyKey !== void 0) return payload.idempotencyKey;
52
52
  if (payload.idempotencyKeyPrefix !== void 0) return `${payload.idempotencyKeyPrefix}:${jobId}`;
53
53
  }
54
- //#endregion
55
- //#region src/pg-boss/queue.ts
54
+ // src/pg-boss/queue.ts
56
55
  function dlqName(queueName) {
57
56
  return `${queueName}-dlq`;
58
57
  }
@@ -323,8 +322,7 @@ function createPgBossDlqWorker(deps) {
323
322
  stop
324
323
  };
325
324
  }
326
- //#endregion
327
- //#region src/pg-boss/scheduler.ts
325
+ // src/pg-boss/scheduler.ts
328
326
  /** Create the start queue (idempotent). */
329
327
  async function ensureStartQueue(boss, queueName) {
330
328
  try {
@@ -432,5 +430,4 @@ function createPgBossStartWorker(deps) {
432
430
  stop
433
431
  };
434
432
  }
435
- //#endregion
436
433
  export { DEFAULT_STEP_QUEUE_CONFIG, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue, resolveStartIdempotencyKey };
@@ -1,8 +1,7 @@
1
- import { ft as WorkflowId } from "../index-BlwtmRJP.js";
2
- import { f as WorkflowStore } from "../store-BPHVI_a3.js";
3
- import { g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-BgUtESlj.js";
1
+ import { pt as WorkflowId } from "../index-BBO5BGS8.js";
2
+ import { f as WorkflowStore } from "../store-oCpqAuTT.js";
3
+ import { g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-C-wtcDDo.js";
4
4
  import { Pool } from "pg";
5
- //#region src/postgres/ddl.d.ts
6
5
  /**
7
6
  * Schema for the Postgres WorkflowStore adapter. Two tables mirror flow-core's
8
7
  * record shapes. Apply once at deploy time (or call `applySchema`).
@@ -15,8 +14,6 @@ export declare function flowStoreDdl(schema?: string): string;
15
14
  export declare const FLOW_STORE_DDL: string;
16
15
  /** `CREATE SCHEMA IF NOT EXISTS` line for non-default schemas; empty for 'public'. */
17
16
  export declare function createSchemaDdl(schema: string): string;
18
- //#endregion
19
- //#region src/postgres/executor.d.ts
20
17
  /** Minimal query result shape the adapters consume (a structural subset of `pg`'s `QueryResult`). */
21
18
  export interface SqlResult<R> {
22
19
  rows: R[];
@@ -103,8 +100,6 @@ export declare function pgliteExecutor(db: PgliteDatabaseLike): SqlExecutor;
103
100
  * its `transaction` method (a `pg` `Pool` has none).
104
101
  */
105
102
  export declare function toExecutor(db: Pool | SqlExecutor): SqlExecutor;
106
- //#endregion
107
- //#region src/postgres/store.d.ts
108
103
  export interface PgWorkflowStoreDeps {
109
104
  pool: Pool;
110
105
  /** Partition this store instance is bound to (e.g. a tenant id). */
@@ -141,8 +136,6 @@ export declare function createWorkflowStore(deps: WorkflowStoreDeps): WorkflowSt
141
136
  export declare function createPgWorkflowStore(deps: PgWorkflowStoreDeps): WorkflowStore;
142
137
  /** Convenience: apply the flow-store schema to a database. */
143
138
  export declare function applySchema(pool: Pool, ddl: string): Promise<void>;
144
- //#endregion
145
- //#region src/postgres/gate.d.ts
146
139
  /** DDL for the gate's two tables. Apply once at deploy time (or via `applySchema`). */
147
140
  export declare function flowGateDdl(opts?: {
148
141
  schema?: string;
@@ -189,8 +182,6 @@ export declare function createStepGate(config: StepGateConfig): StepGate;
189
182
  * Level Security should build an executor and call {@link createStepGate}.
190
183
  */
191
184
  export declare function createPgStepGate(config: PgStepGateConfig): StepGate;
192
- //#endregion
193
- //#region src/postgres/event-sink.d.ts
194
185
  /**
195
186
  * DDL for the append-only run-history table. Apply once at deploy time (alongside
196
187
  * `flowStoreDdl`), or call `applySchema(pool, flowEventDdl())`.
@@ -244,5 +235,4 @@ export declare function readFlowEvents(db: Pool | SqlExecutor, params: {
244
235
  workflowId: WorkflowId;
245
236
  partitionKey: string;
246
237
  schema?: string;
247
- }): Promise<FlowEvent[]>;
248
- //#endregion
238
+ }): Promise<FlowEvent[]>;
@@ -1,6 +1,6 @@
1
- import { n as gateRuleKeys, r as UNSETTLED_STEP_STATUSES } from "../gate-BBZK5OgY.js";
2
- import "../core-COMWflJl.js";
3
- //#region src/postgres/ddl.ts
1
+ import { n as gateRuleKeys, r as UNSETTLED_STEP_STATUSES } from "../gate-BcjK7FoL.js";
2
+ import "../core-ut8VZ0sU.js";
3
+ // src/postgres/ddl.ts
4
4
  /**
5
5
  * Schema for the Postgres WorkflowStore adapter. Two tables mirror flow-core's
6
6
  * record shapes. Apply once at deploy time (or call `applySchema`).
@@ -80,8 +80,7 @@ const FLOW_STORE_DDL = flowStoreDdl();
80
80
  function createSchemaDdl(schema) {
81
81
  return schema === "public" ? "" : `CREATE SCHEMA IF NOT EXISTS ${schema};\n\n`;
82
82
  }
83
- //#endregion
84
- //#region src/postgres/executor.ts
83
+ // src/postgres/executor.ts
85
84
  /**
86
85
  * Build a {@link SqlExecutor} over a `pg` {@link Pool}: top-level queries run on
87
86
  * the pool (autocommit); `transaction` checks out a client and wraps `fn` in
@@ -195,8 +194,7 @@ function pgliteExecutor(db) {
195
194
  function toExecutor(db) {
196
195
  return "transaction" in db ? db : poolExecutor(db);
197
196
  }
198
- //#endregion
199
- //#region src/postgres/store.ts
197
+ // src/postgres/store.ts
200
198
  const iso = (v) => v == null ? null : v instanceof Date ? v.toISOString() : v;
201
199
  function mapWorkflow(r) {
202
200
  return {
@@ -322,6 +320,15 @@ function createWorkflowStore(deps) {
322
320
  partitionKey
323
321
  ])).rowCount ?? 0) > 0;
324
322
  }
323
+ async function saveStepCheckpoint(stepId, attempt, value) {
324
+ return ((await exec.query(`UPDATE ${STEP} SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{checkpoint}', $2::text::jsonb)
325
+ WHERE id = $1 AND partition_key = $3 AND status = 'running' AND attempts = $4`, [
326
+ stepId,
327
+ JSON.stringify(value ?? null, (_k, v) => typeof v === "string" ? v.replaceAll("\0", "") : v),
328
+ partitionKey,
329
+ attempt
330
+ ])).rowCount ?? 0) > 0;
331
+ }
325
332
  async function heartbeatStep(stepId, at) {
326
333
  return ((await exec.query(`UPDATE ${STEP} s SET heartbeat_at = $2
327
334
  FROM ${WF} w
@@ -334,6 +341,14 @@ function createWorkflowStore(deps) {
334
341
  partitionKey
335
342
  ])).rowCount ?? 0) > 0;
336
343
  }
344
+ async function releaseStep(stepId, attempt) {
345
+ return ((await exec.query(`UPDATE ${STEP} SET status = 'pending', attempts = attempts - 1, heartbeat_at = NULL
346
+ WHERE id = $1 AND partition_key = $2 AND status = 'running' AND attempts = $3`, [
347
+ stepId,
348
+ partitionKey,
349
+ attempt
350
+ ])).rowCount ?? 0) > 0;
351
+ }
337
352
  async function markStepPending(stepId) {
338
353
  await exec.query(`UPDATE ${STEP} SET status = 'pending' WHERE id = $1 AND partition_key = $2`, [stepId, partitionKey]);
339
354
  }
@@ -569,7 +584,9 @@ function createWorkflowStore(deps) {
569
584
  listSteps,
570
585
  markStepRunning,
571
586
  heartbeatStep,
587
+ saveStepCheckpoint,
572
588
  markStepPending,
589
+ releaseStep,
573
590
  markStepWaiting,
574
591
  markStepMapping,
575
592
  markStepCompensating,
@@ -609,8 +626,7 @@ function createPgWorkflowStore(deps) {
609
626
  async function applySchema(pool, ddl) {
610
627
  await pool.query(ddl);
611
628
  }
612
- //#endregion
613
- //#region src/postgres/gate.ts
629
+ // src/postgres/gate.ts
614
630
  const DEFAULT_RATE_TABLE = "flow_rate_bucket";
615
631
  const DEFAULT_LEASE_TABLE = "flow_step_lease";
616
632
  /** DDL for the gate's two tables. Apply once at deploy time (or via `applySchema`). */
@@ -768,8 +784,7 @@ function createPgStepGate(config) {
768
784
  ...rest
769
785
  });
770
786
  }
771
- //#endregion
772
- //#region src/postgres/event-sink.ts
787
+ // src/postgres/event-sink.ts
773
788
  /**
774
789
  * DDL for the append-only run-history table. Apply once at deploy time (alongside
775
790
  * `flowStoreDdl`), or call `applySchema(pool, flowEventDdl())`.
@@ -868,5 +883,4 @@ async function readFlowEvents(db, params) {
868
883
  error: r.error ?? void 0
869
884
  }));
870
885
  }
871
- //#endregion
872
886
  export { FLOW_EVENT_DDL, FLOW_GATE_DDL, FLOW_STORE_DDL, applySchema, createEventSink, createPgEventSink, createPgStepGate, createPgWorkflowStore, createSchemaDdl, createStepGate, createWorkflowStore, flowEventDdl, flowGateDdl, flowStoreDdl, pgliteExecutor, poolExecutor, postgresJsExecutor, readFlowEvents, toExecutor };
@@ -1,4 +1,4 @@
1
- //#region src/core/result.ts
1
+ // src/core/result.ts
2
2
  const ok = (value) => ({
3
3
  ok: true,
4
4
  value
@@ -7,5 +7,4 @@ const err = (error) => ({
7
7
  ok: false,
8
8
  error
9
9
  });
10
- //#endregion
11
10
  export { ok as n, err as t };
@@ -1,5 +1,4 @@
1
- import { at as StepRecord, ft as WorkflowId, gt as WorkflowStatus, ht as WorkflowRecord, rt as StepId, vt as WorkflowWithSteps } from "./index-BlwtmRJP.js";
2
- //#region src/core/store.d.ts
1
+ import { _t as WorkflowStatus, gt as WorkflowRecord, it as StepId, ot as StepRecord, pt as WorkflowId, yt as WorkflowWithSteps } from "./index-BBO5BGS8.js";
3
2
  interface CreateWorkflowStep {
4
3
  key: string;
5
4
  type: string;
@@ -171,6 +170,16 @@ interface WorkflowStore {
171
170
  markStepRunning(stepId: StepId, startedAt: string): Promise<boolean>;
172
171
  /** Reset a step to `pending` for a scheduled retry (leaves `attempts` as-is). */
173
172
  markStepPending(stepId: StepId): Promise<void>;
173
+ /**
174
+ * Hand a running step back to `pending` **without spending the attempt** — a
175
+ * handler that continues later (`continueStep`): `attempts` goes back by one,
176
+ * and the heartbeat is cleared. Returns whether it wrote.
177
+ *
178
+ * Implementations MUST write only while the step is `running` on `attempt`
179
+ * (one conditional statement), like `saveStepCheckpoint`: a worker the sweeper
180
+ * gave up on must not hand back a step another attempt now runs.
181
+ */
182
+ releaseStep(stepId: StepId, attempt: number): Promise<boolean>;
174
183
  /**
175
184
  * Stamp `heartbeatAt` on a step whose handler is still working, and report
176
185
  * whether that step is still the caller's to run.
@@ -188,6 +197,18 @@ interface WorkflowStore {
188
197
  * they own it. One statement, not a read-then-write: the sweeper races it.
189
198
  */
190
199
  heartbeatStep(stepId: StepId, at: string): Promise<boolean>;
200
+ /**
201
+ * Keep what a running step has done so far, for the next attempt of the same
202
+ * step: `value` (JSON) under `metadata.checkpoint`, replacing the last one.
203
+ * Returns whether it was written.
204
+ *
205
+ * Implementations MUST write only while the step is `running` **and** still on
206
+ * `attempt` (its `attempts` counter) — one conditional statement — so a worker
207
+ * the sweeper has given up on cannot overwrite what the attempt that replaced
208
+ * it keeps. Optional: a store without it gives steps a checkpoint that never
209
+ * saves, and they start over on a retry as before.
210
+ */
211
+ saveStepCheckpoint?(stepId: StepId, attempt: number, value: unknown): Promise<boolean>;
191
212
  /**
192
213
  * Flip a ready step to `waiting` — it suspends until `resumeStep` (waitForEvent)
193
214
  * or a sub-workflow child settles. `waitingAt` is stamped on `startedAt`: it is
@@ -276,5 +297,4 @@ interface WorkflowStore {
276
297
  */
277
298
  findStuckSteps(workflowId: WorkflowId, cutoff: string): Promise<StepRecord[]>;
278
299
  }
279
- //#endregion
280
300
  export { CreatedWorkflow as a, ListWorkflowsFilters as c, UNSETTLED_STEP_STATUSES as d, WorkflowStore as f, CreateWorkflowStep as i, ReopenWorkflowParams as l, CompleteStepParams as n, FailStepParams as o, WorkflowTreeFilter as p, CreateWorkflowParams as r, FinishWorkflowParams as s, AddChildStep as t, TransactionalScope as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octaflow",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss. The model layer is @octabits-io/models.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -80,7 +80,7 @@
80
80
  }
81
81
  },
82
82
  "devDependencies": {
83
- "@electric-sql/pglite": "0.5.10",
83
+ "@electric-sql/pglite": "0.5.12",
84
84
  "@testcontainers/postgresql": "^12.2.1",
85
85
  "@types/node": "^26.6.5",
86
86
  "@types/pg": "^8.23.1",
@@ -61,7 +61,7 @@ export const caseLoop = defineCaseLoop({
61
61
  manager: {
62
62
  system: 'You decide the next move, one at a time, by calling exactly one decision tool. …',
63
63
  prompt: (ctx, result, facts) => caseFile(ctx.workflowInput, result, facts),
64
- tools: (ctx) => readOnlyTools(ctx), // the manager looks things up; moves do the work
64
+ tools: (ctx) => readOnlyTools(ctx), // agentTool('reads', tool({ … })) each — moves do the work
65
65
  resources: ['llm'],
66
66
  retry: { maxAttempts: 3 },
67
67
  },
@@ -129,7 +129,7 @@ and "closes the case as stuck when the manager cannot decide".
129
129
 
130
130
  - [ ] Every exit has a `why`; every non-move exit has a closer
131
131
  - [ ] The policy is in `refuse` and reads facts from the store, not from the model's words
132
- - [ ] The manager's tools only read
132
+ - [ ] The manager's tools only read (`agentTool('reads', …)`); an `outward` one is refused without `propose`
133
133
  - [ ] Every outward step waits for a person and sends exactly what they released
134
134
  - [ ] `startMove` passes the `caseId:moveNo` idempotency key
135
135
  - [ ] The manager's model goes through the disclosure gate