octaflow 0.36.0 → 0.37.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
@@ -215,8 +215,59 @@ next to what your steps actually do.
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
217
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
218
+ | 📄 | **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) |
218
219
  | 🤖 | **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) |
219
- | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
220
+ | 🏁 | **Settled once** — a step's completion and a run's finish are conditional writes: an event racing its deadline, two last parallel steps, a cancel racing the last completion — one wins, and only the winner emits, compensates and runs hooks |
221
+ | 🧱 | **Pluggable everything** — `WorkflowStore` (its settling writes return whether they won), `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
222
+
223
+ ## Workflow documents
224
+
225
+ `octaflow/document` holds a workflow as **data**: a person, a template catalog or a model writes
226
+ a JSON document, `compileWorkflowDocument` checks it and returns the `WorkflowDefinition` the
227
+ engine runs, or every finding with the path it is about. Behaviour stays in code: a document
228
+ picks, wires and fills **kinds** a host registered once (`defineKind`), and each step carries its
229
+ spec in its own input, so a template saved a minute ago runs on a worker that has never seen it.
230
+
231
+ ```ts
232
+ import { compileWorkflowDocument, coreKinds, defineKind, registerWorkflowKinds, resumeDocumentStep } from 'octaflow/document';
233
+
234
+ const mailSend = defineKind({
235
+ kind: 'mail.send',
236
+ params: { to: z.string(), subject: z.string(), body: z.string() },
237
+ effect: 'outward', // the compiler refuses a document where no person's approval stands before it
238
+ output: z.object({ messageId: z.string() }),
239
+ handler: async ({ params }) => ({ messageId: await mail.send(params) }),
240
+ });
241
+ const kinds = [...coreKinds, mailSend];
242
+ registerWorkflowKinds(registry, kinds); // once, at startup
243
+
244
+ const compiled = compileWorkflowDocument(templateJson, kinds);
245
+ if (!compiled.ok) return compiled.error.issues; // [{ code, path: 'steps.send.params.body', message }]
246
+ await engine.startWorkflow(compiled.value.definition, compiled.value.inputSchema.parse(input));
247
+ // later, a person answers a waiting step — checked against what it declared first
248
+ await resumeDocumentStep(engine, compiled.value, workflowId, 'approve', { decision: 'approve', value: edited });
249
+ ```
250
+
251
+ Steps with dependencies (`after`, and every step an expression reads), `when` guards, `join`,
252
+ `flow` to group steps or run another document by `ref`; declared types are a JSON Schema subset;
253
+ expressions are total and side-effect free, with durations (`days(input.days)`) and exact
254
+ decimals (`decimal(input.amount)`) as types of their own.
255
+
256
+ | Core kind | What it does |
257
+ |---|---|
258
+ | `sleep` | a durable wait, timed from the run's input or an earlier step |
259
+ | `event.wait` | suspends until something outside resumes it |
260
+ | `human.task` / `human.approve` | a person's work; an approval, which an outward step must have before it |
261
+ | `decide` | a rule table: the first rule that holds gives the answer |
262
+ | `map` | its steps once per item, each item a run of its own |
263
+ | `repeat` | its steps pass after pass while a condition holds, up to a cap — one step of one run |
264
+ | `flow` | groups steps, or runs another document (`ref`, pinned to a revision) as a sub-workflow |
265
+
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
+ `reviewWorkflowRevision` lists what changed in it, and `checkPublication` wants someone other
269
+ than the author to approve a change. The format and its decisions are in
270
+ [docs/workflow-documents.md](./docs/workflow-documents.md).
220
271
 
221
272
  ---
222
273
 
@@ -1,112 +1,4 @@
1
1
  import { z } from "zod";
2
- //#region src/core/gate.ts
3
- /**
4
- * The rule keys a request is held to, in the order they are checked: the
5
- * step's type, then each resource it declares, without repeats. The shipped
6
- * gates look up their `concurrency` and `rateLimit` rules by these keys, so a
7
- * rule keyed `'llm:local'` holds every step that names that resource, whatever
8
- * its type, and a rule keyed by a type still holds that type alone. A custom
9
- * gate that wants the same vocabulary calls this.
10
- */
11
- function gateRuleKeys(req) {
12
- const keys = [req.stepType];
13
- for (const resource of req.resources ?? []) if (!keys.includes(resource)) keys.push(resource);
14
- return keys;
15
- }
16
- /**
17
- * A per-process {@link StepGate}: concurrency caps (held slots) plus token-bucket
18
- * rate limits (throughput), each keyed by a step type or by a resource a step
19
- * declares ({@link gateRuleKeys}). Both are optional; a step none of whose keys
20
- * has a rule is always admitted, and a step with several keys must pass every
21
- * rule among them.
22
- *
23
- * Concurrency is reserved first, then the rate tokens — so a rate denial does not
24
- * waste a concurrency slot, and a denial by a later rule gives back what earlier
25
- * ones granted. Good for single-worker deployments; swap in a shared-store gate
26
- * for multi-worker concurrency control.
27
- */
28
- function createInMemoryStepGate(config = {}) {
29
- const now = config.now ?? (() => Date.now());
30
- const concurrencyRetry = Math.max(1, config.concurrencyRetrySeconds ?? 1);
31
- const running = /* @__PURE__ */ new Map();
32
- const buckets = /* @__PURE__ */ new Map();
33
- const capacityOf = (rule) => rule.burst ?? Math.max(1, Math.ceil(rule.perSecond));
34
- function rateCheck(key) {
35
- const rule = config.rateLimit?.[key];
36
- if (!rule) return { ok: true };
37
- const capacity = capacityOf(rule);
38
- const tNow = now();
39
- let b = buckets.get(key);
40
- if (!b) {
41
- b = {
42
- tokens: capacity,
43
- updatedMs: tNow
44
- };
45
- buckets.set(key, b);
46
- }
47
- const elapsedSec = Math.max(0, (tNow - b.updatedMs) / 1e3);
48
- b.tokens = Math.min(capacity, b.tokens + elapsedSec * rule.perSecond);
49
- b.updatedMs = tNow;
50
- if (b.tokens >= 1) {
51
- b.tokens -= 1;
52
- return { ok: true };
53
- }
54
- const needed = 1 - b.tokens;
55
- return {
56
- ok: false,
57
- retryAfterSeconds: Math.max(1, Math.ceil(needed / rule.perSecond))
58
- };
59
- }
60
- /** Give back a token a denied request had already taken from an earlier bucket. */
61
- function refund(key) {
62
- const rule = config.rateLimit?.[key];
63
- const b = buckets.get(key);
64
- if (rule && b) b.tokens = Math.min(capacityOf(rule), b.tokens + 1);
65
- }
66
- return { async acquire(req) {
67
- const keys = gateRuleKeys(req);
68
- const held = [];
69
- let released = false;
70
- const release = () => {
71
- if (released) return;
72
- released = true;
73
- for (const key of held) running.set(key, Math.max(0, (running.get(key) ?? 1) - 1));
74
- };
75
- for (const key of keys) {
76
- const cRule = config.concurrency?.[key];
77
- if (!cRule) continue;
78
- const current = running.get(key) ?? 0;
79
- if (current >= cRule.maxConcurrent) {
80
- release();
81
- return {
82
- admitted: false,
83
- retryAfterSeconds: concurrencyRetry
84
- };
85
- }
86
- running.set(key, current + 1);
87
- held.push(key);
88
- }
89
- const taken = [];
90
- for (const key of keys) {
91
- if (!config.rateLimit?.[key]) continue;
92
- const rate = rateCheck(key);
93
- if (!rate.ok) {
94
- for (const t of taken) refund(t);
95
- release();
96
- return {
97
- admitted: false,
98
- retryAfterSeconds: rate.retryAfterSeconds
99
- };
100
- }
101
- taken.push(key);
102
- }
103
- return {
104
- admitted: true,
105
- release
106
- };
107
- } };
108
- }
109
- //#endregion
110
2
  //#region src/core/retry.ts
111
3
  /**
112
4
  * Realm-safe marker for an explicit retryability decision. `Symbol.for` so a
@@ -990,4 +882,4 @@ function toPublicWorkflow(workflow) {
990
882
  };
991
883
  }
992
884
  //#endregion
993
- export { nonRetryableError as C, gateRuleKeys as E, markRetryable as S, createInMemoryStepGate 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 };
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 };
@@ -1,4 +1,4 @@
1
- import { St as Result, lt as WorkflowId, nt as StepId, xt as FlowErrorShape } from "./index-DZUO24Rz.js";
1
+ import { Tt as Result, ft as WorkflowId, rt as StepId, wt as FlowErrorShape } from "./index-BlwtmRJP.js";
2
2
  //#region src/core/logger.d.ts
3
3
  type LogAttributes = Record<string, unknown>;
4
4
  interface Logger {
@@ -0,0 +1,471 @@
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";
3
+ import { z } from "zod";
4
+ //#region src/document/schema.d.ts
5
+ /** A schema of the subset, or one the checker derived. `{}` is "anything". */
6
+ type JsonSchema = {
7
+ type?: 'object' | 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'null' | 'duration' | 'decimal';
8
+ properties?: Record<string, JsonSchema>;
9
+ required?: string[];
10
+ items?: JsonSchema;
11
+ enum?: ReadonlyArray<string | number | boolean>;
12
+ format?: string;
13
+ [keyword: string]: unknown;
14
+ };
15
+ /** A finding with the JSON path it is about. */
16
+ interface PathedIssue {
17
+ path: string;
18
+ message: string;
19
+ }
20
+ /**
21
+ * Whether a declared schema stays inside the subset. Everything the launch
22
+ * form, the review card and the checker would have to understand is listed;
23
+ * anything else (`$ref`, `oneOf`, `patternProperties`, …) is refused with its path.
24
+ */
25
+ export declare function checkSchemaSubset(schema: unknown, path: string): PathedIssue[];
26
+ /**
27
+ * The validator for a subset schema. `format: "decimal"` (money, exact numbers)
28
+ * is checked here, since JSON Schema has no such format; Zod builds the rest.
29
+ * Cached by the schema's text, so a document's steps share one validator each.
30
+ */
31
+ export declare function zodFor(schema: JsonSchema): z.ZodType;
32
+ //#endregion
33
+ //#region src/document/format.d.ts
34
+ export declare const WORKFLOW_DOCUMENT_FORMAT: 'octaflow.workflow';
35
+ export declare const WORKFLOW_DOCUMENT_VERSION: 1;
36
+ interface DocumentStep {
37
+ kind: string;
38
+ label?: string;
39
+ after?: string[];
40
+ when?: string;
41
+ join?: 'all' | 'any';
42
+ params?: Record<string, unknown>;
43
+ output?: Record<string, unknown>;
44
+ timeout?: string;
45
+ onTimeout?: 'fail' | {
46
+ output: Record<string, unknown>;
47
+ };
48
+ onFailure?: 'fail' | {
49
+ output: Record<string, unknown>;
50
+ };
51
+ retry?: {
52
+ maxAttempts: number;
53
+ backoff?: 'fixed' | 'exponential';
54
+ initialDelayMs?: number;
55
+ maxDelayMs?: number;
56
+ };
57
+ /** For `flow`, `map` and `repeat`: the steps it groups, or runs per item, or per pass. */
58
+ steps?: Record<string, DocumentStep>;
59
+ /** For `flow` only: another document to run as a sub-workflow, pinned to a revision. */
60
+ ref?: {
61
+ id: string;
62
+ revision: number;
63
+ };
64
+ }
65
+ export declare const documentStepSchema: z.ZodType<DocumentStep>;
66
+ export declare const roleSchema: z.ZodObject<{
67
+ id: z.ZodString;
68
+ label: z.ZodString;
69
+ may: z.ZodArray<z.ZodEnum<{
70
+ agent: "agent";
71
+ person: "person";
72
+ }>>;
73
+ }, z.core.$strict>;
74
+ export declare const workflowDocumentSchema: z.ZodObject<{
75
+ format: z.ZodLiteral<"octaflow.workflow">;
76
+ version: z.ZodLiteral<1>;
77
+ id: z.ZodString;
78
+ revision: z.ZodNumber;
79
+ name: z.ZodString;
80
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
81
+ roles: z.ZodOptional<z.ZodArray<z.ZodObject<{
82
+ id: z.ZodString;
83
+ label: z.ZodString;
84
+ may: z.ZodArray<z.ZodEnum<{
85
+ agent: "agent";
86
+ person: "person";
87
+ }>>;
88
+ }, z.core.$strict>>>;
89
+ steps: z.ZodRecord<z.ZodString, z.ZodType<DocumentStep, unknown, z.core.$ZodTypeInternals<DocumentStep, unknown>>>;
90
+ result: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
91
+ ui: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
92
+ }, z.core.$strict>;
93
+ type WorkflowDocument = z.infer<typeof workflowDocumentSchema>;
94
+ type DocumentRole = z.infer<typeof roleSchema>;
95
+ //#endregion
96
+ //#region src/document/kinds.d.ts
97
+ /** Who acts in a step of this kind; a step's `role` must allow it. */
98
+ type KindActor = 'agent' | 'person' | 'system';
99
+ /**
100
+ * What a step of this kind does to the world. `outward` — a mail, a payment, a
101
+ * post — needs a person's approval on every path to it, and the compiler
102
+ * refuses a document where it hasn't one.
103
+ */
104
+ type KindEffect = 'none' | 'internal' | 'outward';
105
+ /** How a kind's output type is known. */
106
+ type KindOutput =
107
+ /** The step declares it (`output`), required. */
108
+ 'declared' |
109
+ /** The step may declare it; otherwise any object. */
110
+ 'declared-optional' |
111
+ /** Fixed by the kind. */
112
+ z.ZodType |
113
+ /** Derived from the types of the step's params, as the compiler resolved them. */
114
+ ((args: {
115
+ paramTypes: Record<string, JsonSchema>;
116
+ params: Record<string, unknown>;
117
+ }) => JsonSchema);
118
+ /**
119
+ * A kind's own static checks, beyond its params schema: what a kind carries
120
+ * that only it understands — the policy rules of an agent's exits, say. Paths
121
+ * are relative to the step's params.
122
+ */
123
+ type KindCheck = (args: {
124
+ params: Record<string, unknown>;
125
+ /** The types the step's expressions see: `input`, `steps`. */
126
+ scope: Record<string, JsonSchema>;
127
+ /** Check an expression under the step's scope plus any names the kind adds. */
128
+ checkExpr: (src: string, extra?: Record<string, JsonSchema>) => {
129
+ type: JsonSchema;
130
+ errors: string[];
131
+ };
132
+ }) => Array<{
133
+ path: string;
134
+ message: string;
135
+ }>;
136
+ interface KindHandlerArgs<TParams, TContext> {
137
+ /** The step's params, every binding resolved, parsed by the kind's schema. */
138
+ params: TParams;
139
+ /** The step's output type: what the handler's result is validated against. */
140
+ output: JsonSchema;
141
+ stepKey: string;
142
+ /** What the step's expressions see — `input`, `steps` — for a kind that evaluates expressions of its own. */
143
+ scope: Record<string, unknown>;
144
+ ctx: StepExecutionContext<TContext>;
145
+ }
146
+ interface WorkflowKind<TContext = unknown> {
147
+ readonly kind: string;
148
+ readonly params: z.ZodObject<z.ZodRawShape>;
149
+ readonly actor: KindActor;
150
+ readonly effect: KindEffect;
151
+ /** A person's approval: it is what makes an outward step downstream of it allowed. */
152
+ readonly approval: boolean;
153
+ /** Suspends until the host resumes it (a person, an answer from outside). */
154
+ readonly wait: boolean;
155
+ /** A durable sleep: the delay in ms from params resolved against the run's input. */
156
+ readonly sleep?: (params: Record<string, unknown>) => number;
157
+ readonly output: KindOutput;
158
+ readonly check?: KindCheck;
159
+ /** A kind with a body the compiler compiles (`map`, `repeat`, a `flow` by ref) — core kinds only. */
160
+ readonly structural?: 'map' | 'repeat' | 'subflow';
161
+ /**
162
+ * A kind that stands for several steps: the compiler replaces the step with
163
+ * these, as it does a flow's — the ones that wait for none of the others
164
+ * start, carrying the step's `after` and `when`. One of them should keep the
165
+ * step's key, so `steps.<key>` still reads what the step stands for.
166
+ */
167
+ readonly expand?: (key: string, step: DocumentStep) => Record<string, DocumentStep>;
168
+ /**
169
+ * The expressions a kind keeps in its params as text (a rule's `when`, a
170
+ * policy's `refuse`). What they read is what the step waits for, as with
171
+ * bindings — so the compiler asks.
172
+ */
173
+ readonly reads?: (params: Record<string, unknown>) => string[];
174
+ readonly handler?: (args: KindHandlerArgs<Record<string, unknown>, TContext>) => Promise<Record<string, unknown>>;
175
+ readonly retry?: RetryPolicy;
176
+ readonly heartbeatTimeoutMs?: number;
177
+ readonly resources?: readonly string[];
178
+ }
179
+ interface KindConfig<TParams extends z.ZodRawShape, TContext> {
180
+ kind: string;
181
+ params: TParams;
182
+ actor?: KindActor;
183
+ effect?: KindEffect;
184
+ approval?: boolean;
185
+ wait?: boolean;
186
+ sleep?: (params: z.infer<z.ZodObject<TParams>>) => number;
187
+ output: KindOutput;
188
+ check?: KindCheck;
189
+ /** @internal */
190
+ structural?: 'map' | 'repeat' | 'subflow';
191
+ expand?: (key: string, step: DocumentStep) => Record<string, DocumentStep>;
192
+ reads?: (params: Record<string, unknown>) => string[];
193
+ handler?: (args: KindHandlerArgs<z.infer<z.ZodObject<TParams>>, TContext>) => Promise<Record<string, unknown>>;
194
+ retry?: RetryPolicy;
195
+ heartbeatTimeoutMs?: number;
196
+ resources?: readonly string[];
197
+ }
198
+ export declare function defineKind<TParams extends z.ZodRawShape, TContext = unknown>(config: KindConfig<TParams, TContext>): WorkflowKind<TContext>;
199
+ /** A durable sleep. `for` is an ISO 8601 duration or an expression over the run's input. */
200
+ export declare const sleepKind: WorkflowKind<unknown>;
201
+ /** Suspends until something outside resumes it with an event — a customer's reply, a webhook. */
202
+ export declare const eventWaitKind: WorkflowKind<unknown>;
203
+ /** A person does a piece of work and hands back what the step declares. */
204
+ export declare const humanTaskKind: WorkflowKind<unknown>;
205
+ /**
206
+ * A person approves or rejects `subject`, and may hand back an edited version
207
+ * of it as `value`. An outward step reads the approved value, never the draft.
208
+ */
209
+ export declare const humanApproveKind: WorkflowKind<unknown>;
210
+ /**
211
+ * Runs its `steps` once per item of `over`, each item as a sub-workflow of its
212
+ * own; the body reads `input.item`, `input.index` and `input.with` (values the
213
+ * step passes in). Outputs `{ items }`, one body output per item, in order.
214
+ */
215
+ export declare const mapKind: WorkflowKind<unknown>;
216
+ /**
217
+ * Runs its `steps` as a pass, then again while `while` holds — an expression
218
+ * over `last` (the pass's output, by step) and `pass` — up to `max` passes. One
219
+ * step of one run, however many passes. The body reads `input.pass`,
220
+ * `input.last` and `input.with`. Outputs `{ passes, last }`.
221
+ */
222
+ export declare const repeatKind: WorkflowKind<unknown>;
223
+ /** A `flow` by `ref`: the referenced document as a sub-workflow, its `input` the step's. Outputs the child's steps' outputs. */
224
+ export declare const subflowKind: WorkflowKind<unknown>;
225
+ /**
226
+ * A deterministic decision: the first rule whose `when` holds gives the answer,
227
+ * `otherwise` when none does. Outputs the answer's fields and `rule` — which one
228
+ * decided, -1 for `otherwise`. A policy written down, not a model's guess.
229
+ */
230
+ export declare const decideKind: WorkflowKind<unknown>;
231
+ export declare const coreKinds: readonly WorkflowKind[];
232
+ //#endregion
233
+ //#region src/document/compile.d.ts
234
+ interface CompileIssue extends PathedIssue {
235
+ code: CompileIssueCode;
236
+ }
237
+ type CompileIssueCode = 'shape' | 'schema' | 'unknown_kind' | 'params' | 'expression' | 'graph' | 'unbounded_wait' | 'role' | 'approval_before_impact';
238
+ /** What a compiled step carries in its static input: everything the generic handler needs to run it. */
239
+ interface CompiledStepSpec {
240
+ kind: string;
241
+ params: Record<string, unknown>;
242
+ when?: string;
243
+ output: JsonSchema;
244
+ /** The engine policies the step sets for itself — deadline, stand-ins, retry, the body it starts. */
245
+ policy: Omit<StepPolicy, 'repeat'>;
246
+ /** For a `repeat` step: the cap and the expression (over `last` and `pass`) that asks for another pass. */
247
+ repeat?: {
248
+ max: number;
249
+ while: string;
250
+ };
251
+ }
252
+ interface CompileOptions {
253
+ /**
254
+ * The documents a `flow` may refer to by `ref: { id, revision }`. Return the
255
+ * document, or undefined when there is none. A run is pinned to the revision
256
+ * named, so a ref names one.
257
+ */
258
+ documents?: (ref: {
259
+ id: string;
260
+ revision: number;
261
+ }) => unknown;
262
+ /** @internal — a body's input is derived by the compiler, not declared; the subset check is for declared types. */
263
+ derivedInput?: boolean;
264
+ /** @internal — the documents being compiled above this one, against a ref cycle. */
265
+ refChain?: string[];
266
+ }
267
+ /** The key under which a compiled step's spec rides in its static input. */
268
+ export declare const SPEC_KEY: '$workflowDocument';
269
+ interface CompiledWorkflow {
270
+ /** `<id>@<revision>`: a run is pinned to the revision it started on. */
271
+ type: string;
272
+ definition: WorkflowDefinition;
273
+ inputSchema: z.ZodType<Record<string, unknown>>;
274
+ /** Each step's spec, by key — what a host reads to resume a waiting step. */
275
+ steps: Record<string, CompiledStepSpec>;
276
+ /** The run's result, as expressions over the steps' outputs. */
277
+ result: Record<string, string>;
278
+ /** Findings that don't stop the document, e.g. a merge that will skip with a guarded branch. */
279
+ warnings: CompileIssue[];
280
+ }
281
+ /** The step type a kind runs under; a merge (`join: "any"`) is its own type, since the join is the type's. */
282
+ export declare function stepTypeOf(kind: string, join?: 'all' | 'any'): string;
283
+ /** A document that doesn't compile: every finding, each with the path it is about. */
284
+ interface InvalidWorkflowDocumentError {
285
+ key: 'invalid_workflow_document';
286
+ message: string;
287
+ issues: CompileIssue[];
288
+ }
289
+ 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
+ * The spec of a document step, from its record's input — what a model resolver
294
+ * reads to pick the model for a step's `role` (`spec.params.role`). Null for a
295
+ * step that isn't a document's.
296
+ */
297
+ export declare function documentStepSpec(step: {
298
+ input: Record<string, unknown> | null;
299
+ }): CompiledStepSpec | null;
300
+ /** A step's params with every binding (`{ "$": … }`, `{ "$t": … }`) resolved against a scope. */
301
+ export declare function resolveParams(value: unknown, scope: Record<string, unknown>): unknown;
302
+ /**
303
+ * Register every kind on an engine's registry: each under two step types, one
304
+ * per join rule. Call once at startup with the same kinds the documents were
305
+ * compiled against.
306
+ */
307
+ export declare function registerWorkflowKinds<TContext>(registry: StepHandlerRegistry<TContext>, kinds: readonly WorkflowKind<TContext>[]): void;
308
+ interface WaitingDocumentStep {
309
+ key: string;
310
+ kind: string;
311
+ /** The params as the step sees them now: the subject of an approval, the instructions of a task. */
312
+ params: Record<string, unknown>;
313
+ /** What the answer must look like. */
314
+ output: CompiledStepSpec['output'];
315
+ }
316
+ /** A waiting step of a document run, its params resolved — what a review card or a task form shows. */
317
+ export declare function describeWaitingStep(store: WorkflowStore, workflowId: WorkflowId, stepKey: string): Promise<WaitingDocumentStep | null>;
318
+ interface ResumableEngine {
319
+ resumeStep(workflowId: WorkflowId, stepKey: string, payload?: Record<string, unknown>): Promise<Result<void, FlowError>>;
320
+ }
321
+ type ResumeDocumentStepError = FlowError | {
322
+ key: 'invalid_answer';
323
+ message: string;
324
+ issues: Array<{
325
+ path: string;
326
+ message: string;
327
+ }>;
328
+ };
329
+ /**
330
+ * Answer a waiting step, checked against what it declared first: an answer the
331
+ * step can't use is refused here, with its issues, rather than accepted and
332
+ * failing the steps that read it.
333
+ */
334
+ export declare function resumeDocumentStep(engine: ResumableEngine, compiled: Pick<CompiledWorkflow, 'steps'>, workflowId: WorkflowId, stepKey: string, answer: Record<string, unknown>): Promise<Result<void, ResumeDocumentStepError>>;
335
+ /**
336
+ * Answer a waiting step by its own record: the spec rides in the step's input,
337
+ * so this needs no compiled document — what a host uses for steps inside a
338
+ * map item, a repeat pass or a referenced flow, which run as child workflows.
339
+ */
340
+ export declare function resumeWaitingStep(engine: ResumableEngine, store: WorkflowStore, workflowId: WorkflowId, stepKey: string, answer: Record<string, unknown>): Promise<Result<void, ResumeDocumentStepError>>;
341
+ /** The run's result, from its document's `result` expressions over the steps' outputs. */
342
+ 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
+ type Expr = {
346
+ kind: 'literal';
347
+ value: string | number | boolean | null;
348
+ } | {
349
+ kind: 'list';
350
+ items: Expr[];
351
+ } | {
352
+ kind: 'path';
353
+ segments: string[];
354
+ } | {
355
+ kind: 'call';
356
+ name: string;
357
+ args: Expr[];
358
+ } | {
359
+ kind: 'unary';
360
+ op: 'not' | '-';
361
+ operand: Expr;
362
+ } | {
363
+ kind: 'binary';
364
+ op: BinaryOp;
365
+ left: Expr;
366
+ right: Expr;
367
+ } | {
368
+ kind: 'if';
369
+ cond: Expr;
370
+ then: Expr;
371
+ else: Expr;
372
+ };
373
+ type BinaryOp = 'and' | 'or' | '=' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | '+' | '-' | '*' | '/';
374
+ export declare class ExprSyntaxError extends Error {}
375
+ export declare function parseExpr(src: string): Expr;
376
+ export declare function evaluate(e: Expr, scope: Record<string, unknown>): unknown;
377
+ interface CheckResult {
378
+ type: JsonSchema;
379
+ errors: string[];
380
+ }
381
+ /**
382
+ * The type of an expression under a scope of typed names (`input`, `steps`, …),
383
+ * and what is wrong with it: a path that cannot exist, an operation on types
384
+ * that never fit. "Anything" passes, since it might.
385
+ */
386
+ export declare function checkExpr(e: Expr, scope: Record<string, JsonSchema>): CheckResult;
387
+ //#endregion
388
+ //#region src/document/duration.d.ts
389
+ /** A duration as an expression value: tagged, so it never passes for a number. */
390
+ interface Duration {
391
+ readonly $duration: number;
392
+ }
393
+ export declare function duration(ms: number): Duration;
394
+ export declare function isDuration(value: unknown): value is Duration;
395
+ /** An ISO 8601 duration (`P2D`, `PT4H30M`) in ms, or null when it isn't one this format accepts. */
396
+ export declare function parseIsoDuration(text: string): number | null;
397
+ //#endregion
398
+ //#region src/document/decimal.d.ts
399
+ /** A decimal as an expression value: tagged, so it never passes for a number or a string. */
400
+ interface Decimal {
401
+ readonly $decimal: string;
402
+ }
403
+ export declare const DIVISION_SCALE = 9;
404
+ export declare function isDecimal(value: unknown): value is Decimal;
405
+ /** A decimal from a decimal string or a finite number; null for anything else. */
406
+ export declare function toDecimal(value: unknown): Decimal | null;
407
+ //#endregion
408
+ //#region src/document/policy.d.ts
409
+ /** One piece of policy, by where it sits in the document. */
410
+ interface PolicyItem {
411
+ path: string;
412
+ value: unknown;
413
+ }
414
+ interface PolicyChange {
415
+ path: string;
416
+ change: 'added' | 'removed' | 'changed';
417
+ before?: unknown;
418
+ after?: unknown;
419
+ }
420
+ /**
421
+ * The policy in a set of steps (a document's, a body's, a case move's):
422
+ *
423
+ * - every approval step — who approves, after what, under which guard, how long, what silence means;
424
+ * - the guard and the bound params of every outward step — when it acts, and on what;
425
+ * - every `refuse` rule on an agent's exits;
426
+ * - every rule table of a `decide` step;
427
+ * - the cap of every `repeat`.
428
+ *
429
+ * Bodies (flows, maps, repeats) are walked; a referenced document is its own
430
+ * revision and is reviewed on its own.
431
+ */
432
+ export declare function stepsPolicy(steps: Record<string, DocumentStep>, prefix: string, kinds: readonly WorkflowKind<any>[]): PolicyItem[];
433
+ /** The policy of a workflow document: its roles' bounds and its steps'. */
434
+ export declare function workflowPolicy(doc: WorkflowDocument, kinds: readonly WorkflowKind<any>[]): PolicyItem[];
435
+ export declare function diffPolicy(before: PolicyItem[], after: PolicyItem[]): PolicyChange[];
436
+ interface RevisionReview {
437
+ /** `<id>@<revision>` of what is published now, or null for a first publication. */
438
+ from: string | null;
439
+ to: string;
440
+ changes: PolicyChange[];
441
+ /** True when the policy changed — a first publication with any policy in it included. */
442
+ needsSecondApproval: boolean;
443
+ }
444
+ /** What a revision changes in policy against the one published before it (or none). */
445
+ export declare function reviewRevision(published: {
446
+ id: string;
447
+ revision: number;
448
+ policy: PolicyItem[];
449
+ } | null, next: {
450
+ id: string;
451
+ revision: number;
452
+ policy: PolicyItem[];
453
+ }): RevisionReview;
454
+ /** Review a workflow document's new revision. */
455
+ export declare function reviewWorkflowRevision(published: WorkflowDocument | null, next: WorkflowDocument, kinds: readonly WorkflowKind<any>[]): RevisionReview;
456
+ interface PublicationRefused {
457
+ key: 'second_approval_required';
458
+ message: string;
459
+ changes: PolicyChange[];
460
+ }
461
+ /**
462
+ * Whether a revision may be published: when its policy changed, someone other
463
+ * than its author has approved it. The host decides who may approve and keeps
464
+ * the approvals; this holds them to the rule.
465
+ */
466
+ export declare function checkPublication(review: RevisionReview, people: {
467
+ author: string;
468
+ approvedBy: readonly string[];
469
+ }): Result<void, PublicationRefused>;
470
+ //#endregion
471
+ 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 };