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 +52 -1
- package/dist/{core-YqnPwR-v.js → core-COMWflJl.js} +1 -109
- package/dist/{dispatcher-ltwBJjKc.d.ts → dispatcher-jbFVHfdm.d.ts} +1 -1
- package/dist/document/index.d.ts +471 -0
- package/dist/document/index.js +2150 -0
- package/dist/gate-BBZK5OgY.js +122 -0
- package/dist/{index-DZUO24Rz.d.ts → index-BlwtmRJP.d.ts} +55 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +249 -136
- package/dist/observability-BgUtESlj.d.ts +203 -0
- package/dist/pg-boss/index.d.ts +2 -2
- package/dist/postgres/index.d.ts +3 -2
- package/dist/postgres/index.js +36 -14
- package/dist/result-CG_l7xQm.js +11 -0
- package/dist/{observability-BLUEjdto.d.ts → store-BPHVI_a3.d.ts} +42 -209
- package/package.json +8 -2
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
|
-
|
|
|
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,
|
|
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 {
|
|
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 };
|