@nagi-js/core 0.1.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/LICENSE +21 -0
- package/dist/index.d.ts +665 -0
- package/dist/index.js +1285 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lymo, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
type Json = string | number | boolean | null | Json[] | {
|
|
2
|
+
[key: string]: Json;
|
|
3
|
+
};
|
|
4
|
+
type Millis = number;
|
|
5
|
+
type RunId = string & {
|
|
6
|
+
readonly __brand: "RunId";
|
|
7
|
+
};
|
|
8
|
+
type StepId = string;
|
|
9
|
+
type AttemptNumber = number;
|
|
10
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
11
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
12
|
+
}
|
|
13
|
+
declare namespace StandardSchemaV1 {
|
|
14
|
+
interface Props<Input = unknown, Output = Input> {
|
|
15
|
+
readonly version: 1;
|
|
16
|
+
readonly vendor: string;
|
|
17
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
18
|
+
readonly types?: Types<Input, Output>;
|
|
19
|
+
}
|
|
20
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
21
|
+
interface SuccessResult<Output> {
|
|
22
|
+
readonly value: Output;
|
|
23
|
+
readonly issues?: undefined;
|
|
24
|
+
}
|
|
25
|
+
interface FailureResult {
|
|
26
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
27
|
+
}
|
|
28
|
+
interface Issue {
|
|
29
|
+
readonly message: string;
|
|
30
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment>;
|
|
31
|
+
}
|
|
32
|
+
interface PathSegment {
|
|
33
|
+
readonly key: PropertyKey;
|
|
34
|
+
}
|
|
35
|
+
interface Types<Input = unknown, Output = Input> {
|
|
36
|
+
readonly input: Input;
|
|
37
|
+
readonly output: Output;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
type InferSchemaInput<S> = S extends StandardSchemaV1<infer I, unknown> ? I : never;
|
|
41
|
+
type InferSchemaOutput<S> = S extends StandardSchemaV1<unknown, infer O> ? O : never;
|
|
42
|
+
interface Register {
|
|
43
|
+
}
|
|
44
|
+
type Tx = Register extends {
|
|
45
|
+
tx: infer T;
|
|
46
|
+
} ? T : unknown;
|
|
47
|
+
type StepKind = "task" | "signal" | "match";
|
|
48
|
+
interface Step<Output = unknown> {
|
|
49
|
+
readonly kind: StepKind;
|
|
50
|
+
readonly id: StepId;
|
|
51
|
+
readonly __output?: Output;
|
|
52
|
+
}
|
|
53
|
+
type StepOutput<S> = S extends Step<infer O> ? O : never;
|
|
54
|
+
type StepMap = Readonly<Record<string, Step<unknown>>>;
|
|
55
|
+
type NeedsMap = StepMap;
|
|
56
|
+
type NeedsOutputs<N extends NeedsMap> = {
|
|
57
|
+
readonly [K in keyof N]: StepOutput<N[K]>;
|
|
58
|
+
};
|
|
59
|
+
interface Logger {
|
|
60
|
+
debug(message: string, attrs?: Record<string, unknown>): void;
|
|
61
|
+
info(message: string, attrs?: Record<string, unknown>): void;
|
|
62
|
+
warn(message: string, attrs?: Record<string, unknown>): void;
|
|
63
|
+
error(message: string, attrs?: Record<string, unknown>): void;
|
|
64
|
+
}
|
|
65
|
+
interface StepCtx<Input = unknown> {
|
|
66
|
+
readonly input: Input;
|
|
67
|
+
readonly tx: Tx;
|
|
68
|
+
readonly runId: RunId;
|
|
69
|
+
readonly stepId: StepId;
|
|
70
|
+
readonly attempt: AttemptNumber;
|
|
71
|
+
readonly signal: AbortSignal;
|
|
72
|
+
readonly now: () => Date;
|
|
73
|
+
readonly logger: Logger;
|
|
74
|
+
/**
|
|
75
|
+
* Durable per-effect memoization. The first successful call for `(runId, stepId, scope)`
|
|
76
|
+
* persists its return value; subsequent calls (including post-crash retries) return the
|
|
77
|
+
* cached value without re-invoking `fn`.
|
|
78
|
+
*/
|
|
79
|
+
once<T extends Json>(scope: string, fn: () => Promise<T>): Promise<T>;
|
|
80
|
+
/**
|
|
81
|
+
* Stable idempotency key for external APIs (Stripe, Mux, etc.).
|
|
82
|
+
* Returns `hash(runId + stepId + scope)` — identical across retries of the same attempt,
|
|
83
|
+
* different across runs.
|
|
84
|
+
*/
|
|
85
|
+
idempotencyKey(scope: string): string;
|
|
86
|
+
}
|
|
87
|
+
type BackoffStrategy = "exponential" | "linear" | "fixed";
|
|
88
|
+
interface RetryPolicy {
|
|
89
|
+
readonly maxAttempts: number;
|
|
90
|
+
readonly backoff: BackoffStrategy;
|
|
91
|
+
readonly initialDelayMs?: Millis;
|
|
92
|
+
readonly maxDelayMs?: Millis;
|
|
93
|
+
readonly retryOn?: (error: unknown) => boolean;
|
|
94
|
+
}
|
|
95
|
+
interface StepConfigBase<Input, N extends NeedsMap> {
|
|
96
|
+
readonly needs?: N;
|
|
97
|
+
/**
|
|
98
|
+
* Skip this step when the predicate returns false. Locked semantic:
|
|
99
|
+
* downstream steps that need a skipped step are also skipped (transitive).
|
|
100
|
+
* Therefore at the type level, `needs.x` is the unconditional `Output` —
|
|
101
|
+
* not `Output | undefined`.
|
|
102
|
+
*/
|
|
103
|
+
readonly when?: (args: {
|
|
104
|
+
readonly input: NoInfer<Input>;
|
|
105
|
+
readonly needs: NoInfer<NeedsOutputs<N>>;
|
|
106
|
+
}) => boolean;
|
|
107
|
+
readonly timeout?: Millis;
|
|
108
|
+
}
|
|
109
|
+
interface TaskConfig<Input, N extends NeedsMap, Output> extends StepConfigBase<Input, N> {
|
|
110
|
+
readonly retry?: RetryPolicy;
|
|
111
|
+
readonly run: (args: {
|
|
112
|
+
readonly input: NoInfer<Input>;
|
|
113
|
+
readonly needs: NoInfer<NeedsOutputs<N>>;
|
|
114
|
+
readonly ctx: StepCtx<NoInfer<Input>>;
|
|
115
|
+
}) => Promise<Output>;
|
|
116
|
+
}
|
|
117
|
+
interface SignalConfig<Input, N extends NeedsMap, Schema extends StandardSchemaV1> extends StepConfigBase<Input, N> {
|
|
118
|
+
readonly schema: Schema;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Discriminator mode — exhaustive at compile time. `cases` must cover every
|
|
122
|
+
* value in the discriminant union; adding a new value to the discriminant
|
|
123
|
+
* is a type error until the case is handled.
|
|
124
|
+
*/
|
|
125
|
+
interface MatchDiscriminatorConfig<Input, N extends NeedsMap, D extends string, M extends Record<D, StepMap>> {
|
|
126
|
+
readonly needs?: N;
|
|
127
|
+
readonly on: (args: {
|
|
128
|
+
readonly input: NoInfer<Input>;
|
|
129
|
+
readonly needs: NoInfer<NeedsOutputs<N>>;
|
|
130
|
+
}) => D;
|
|
131
|
+
readonly cases: {
|
|
132
|
+
readonly [K in D]: (b: Builder<Input>) => M[K];
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
interface MatchArmGuard<Input, N extends NeedsMap, M extends StepMap> {
|
|
136
|
+
readonly when: (args: {
|
|
137
|
+
readonly input: NoInfer<Input>;
|
|
138
|
+
readonly needs: NoInfer<NeedsOutputs<N>>;
|
|
139
|
+
}) => boolean;
|
|
140
|
+
readonly otherwise?: never;
|
|
141
|
+
readonly build: (b: Builder<Input>) => M;
|
|
142
|
+
}
|
|
143
|
+
interface MatchArmOtherwise<Input, M extends StepMap> {
|
|
144
|
+
readonly otherwise: true;
|
|
145
|
+
readonly when?: never;
|
|
146
|
+
readonly build: (b: Builder<Input>) => M;
|
|
147
|
+
}
|
|
148
|
+
type MatchArm<Input, N extends NeedsMap, M extends StepMap> = MatchArmGuard<Input, N, M> | MatchArmOtherwise<Input, M>;
|
|
149
|
+
/**
|
|
150
|
+
* Inference-friendly arm shape: M is not bound, so each arm in an `arms:` tuple
|
|
151
|
+
* keeps its own StepMap (and per-step Output) through inference. The `arms`
|
|
152
|
+
* overload of `Builder.match` uses this; user-facing arms still satisfy
|
|
153
|
+
* `MatchArm<Input, N, M>` for any concrete M they construct.
|
|
154
|
+
*/
|
|
155
|
+
type MatchArmShape<Input, N extends NeedsMap> = {
|
|
156
|
+
readonly when: (args: {
|
|
157
|
+
readonly input: NoInfer<Input>;
|
|
158
|
+
readonly needs: NoInfer<NeedsOutputs<N>>;
|
|
159
|
+
}) => boolean;
|
|
160
|
+
readonly otherwise?: never;
|
|
161
|
+
readonly build: (b: Builder<Input>) => StepMap;
|
|
162
|
+
} | {
|
|
163
|
+
readonly otherwise: true;
|
|
164
|
+
readonly when?: never;
|
|
165
|
+
readonly build: (b: Builder<Input>) => StepMap;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Guard mode — Rust-style `match { x if guard => ... }`. Top-to-bottom,
|
|
169
|
+
* first match wins. The terminal `{ otherwise: true }` arm is required so
|
|
170
|
+
* guard mode can never silently fall through.
|
|
171
|
+
*/
|
|
172
|
+
interface MatchGuardConfig<Input, N extends NeedsMap, M extends StepMap> {
|
|
173
|
+
readonly needs?: N;
|
|
174
|
+
readonly arms: ReadonlyArray<MatchArm<Input, N, M>>;
|
|
175
|
+
}
|
|
176
|
+
/** The output of a match step is a record of each step in the chosen arm. */
|
|
177
|
+
type MatchArmOutput<M extends StepMap> = {
|
|
178
|
+
readonly [K in keyof M]: StepOutput<M[K]>;
|
|
179
|
+
};
|
|
180
|
+
/** Across all cases of a discriminator match — the union of arm outputs. */
|
|
181
|
+
type MatchDiscriminatorOutput<D extends string, M extends Record<D, StepMap>> = {
|
|
182
|
+
[K in D]: MatchArmOutput<M[K]>;
|
|
183
|
+
}[D];
|
|
184
|
+
/**
|
|
185
|
+
* `b` inside `flow({ build: (b) => ... })`. Constructors return `Step<Output>`
|
|
186
|
+
* values whose Output has been inferred from the handler / schema / arms.
|
|
187
|
+
*/
|
|
188
|
+
interface Builder<Input = unknown> {
|
|
189
|
+
task<N extends NeedsMap, Output>(config: TaskConfig<Input, N, Output>): Step<Output>;
|
|
190
|
+
signal<N extends NeedsMap, S extends StandardSchemaV1>(config: SignalConfig<Input, N, S>): Step<InferSchemaOutput<S>>;
|
|
191
|
+
match<N extends NeedsMap, D extends string, Cases extends {
|
|
192
|
+
readonly [K in D]: (b: Builder<Input>) => StepMap;
|
|
193
|
+
}>(config: {
|
|
194
|
+
readonly needs?: N;
|
|
195
|
+
readonly on: (args: {
|
|
196
|
+
readonly input: NoInfer<Input>;
|
|
197
|
+
readonly needs: NoInfer<NeedsOutputs<N>>;
|
|
198
|
+
}) => D;
|
|
199
|
+
readonly cases: Cases;
|
|
200
|
+
}): Step<{
|
|
201
|
+
readonly [K in keyof Cases]: MatchArmOutput<ReturnType<Cases[K]>>;
|
|
202
|
+
}[keyof Cases]>;
|
|
203
|
+
match<N extends NeedsMap, Arms extends ReadonlyArray<MatchArmShape<Input, N>>>(config: {
|
|
204
|
+
readonly needs?: N;
|
|
205
|
+
readonly arms: Arms;
|
|
206
|
+
}): Step<MatchArmOutput<ReturnType<Arms[number]["build"]>>>;
|
|
207
|
+
}
|
|
208
|
+
interface FlowConfig<Id extends string, InputSchema extends StandardSchemaV1, M extends StepMap, Output = unknown> {
|
|
209
|
+
/** Stable persistence handle. TanStack-key shaped (kebab or snake). */
|
|
210
|
+
readonly id: Id;
|
|
211
|
+
readonly input: InputSchema;
|
|
212
|
+
readonly build: (b: Builder<InferSchemaOutput<InputSchema>>) => M;
|
|
213
|
+
/**
|
|
214
|
+
* Compute the flow's terminal output from its step outputs. Fired once at
|
|
215
|
+
* `flow.completed` and persisted on the fact + `onFlowComplete` event.
|
|
216
|
+
* Skipped steps land as `null` in the input record at runtime even though
|
|
217
|
+
* the type claims otherwise (consistent with the "skip is transitive" lock).
|
|
218
|
+
* Method-shorthand syntax keeps `M` bivariant so `Flow<Id, S, M, O>` remains
|
|
219
|
+
* assignable to `Flow<string, S, StepMap, O>` in fixture/test code.
|
|
220
|
+
*/
|
|
221
|
+
output?(steps: NeedsOutputs<M>): Output;
|
|
222
|
+
}
|
|
223
|
+
interface Flow<Id extends string = string, InputSchema extends StandardSchemaV1 = StandardSchemaV1, M extends StepMap = StepMap, Output = unknown> {
|
|
224
|
+
readonly id: Id;
|
|
225
|
+
readonly input: InputSchema;
|
|
226
|
+
readonly steps: M;
|
|
227
|
+
output?(steps: NeedsOutputs<M>): Output;
|
|
228
|
+
}
|
|
229
|
+
type FlowInput<F> = F extends Flow<string, infer S, StepMap, unknown> ? InferSchemaOutput<S> : never;
|
|
230
|
+
type FlowOutput<F> = F extends Flow<string, StandardSchemaV1, StepMap, infer O> ? O : never;
|
|
231
|
+
interface FlowEvent {
|
|
232
|
+
readonly runId: RunId;
|
|
233
|
+
readonly flowId: string;
|
|
234
|
+
readonly at: Date;
|
|
235
|
+
}
|
|
236
|
+
interface FlowStartEvent extends FlowEvent {
|
|
237
|
+
readonly input: Json;
|
|
238
|
+
}
|
|
239
|
+
interface FlowCompleteEvent extends FlowEvent {
|
|
240
|
+
readonly output: Json;
|
|
241
|
+
}
|
|
242
|
+
interface FlowErrorEvent extends FlowEvent {
|
|
243
|
+
readonly error: SerializedError;
|
|
244
|
+
}
|
|
245
|
+
interface StepEvent extends FlowEvent {
|
|
246
|
+
readonly stepId: StepId;
|
|
247
|
+
readonly attempt: AttemptNumber;
|
|
248
|
+
readonly kind: StepKind;
|
|
249
|
+
}
|
|
250
|
+
interface StepStartEvent extends StepEvent {
|
|
251
|
+
/**
|
|
252
|
+
* The resolved input for this step at the moment it starts.
|
|
253
|
+
* - Task: the value passed to `run({ input, needs, ctx })`.
|
|
254
|
+
* - Signal: `null` (signals await an external payload — no pre-execution input).
|
|
255
|
+
* - Match: `null` (the discriminator value is computed inside the match
|
|
256
|
+
* handler; the start event fires before that resolves).
|
|
257
|
+
*/
|
|
258
|
+
readonly input: Json;
|
|
259
|
+
}
|
|
260
|
+
interface StepCompleteEvent extends StepEvent {
|
|
261
|
+
readonly output: Json;
|
|
262
|
+
readonly durationMs: Millis;
|
|
263
|
+
}
|
|
264
|
+
interface StepErrorEvent extends StepEvent {
|
|
265
|
+
readonly error: SerializedError;
|
|
266
|
+
}
|
|
267
|
+
interface StepRetryEvent extends StepEvent {
|
|
268
|
+
readonly error: SerializedError;
|
|
269
|
+
readonly nextAttemptAt: Date;
|
|
270
|
+
}
|
|
271
|
+
interface SignalSentEvent extends StepEvent {
|
|
272
|
+
readonly payload: Json;
|
|
273
|
+
}
|
|
274
|
+
interface SignalReceivedEvent extends StepEvent {
|
|
275
|
+
readonly payload: Json;
|
|
276
|
+
}
|
|
277
|
+
interface FlowHooks {
|
|
278
|
+
readonly onFlowStart?: (event: FlowStartEvent) => void | Promise<void>;
|
|
279
|
+
readonly onFlowComplete?: (event: FlowCompleteEvent) => void | Promise<void>;
|
|
280
|
+
readonly onFlowError?: (event: FlowErrorEvent) => void | Promise<void>;
|
|
281
|
+
readonly onStepStart?: (event: StepStartEvent) => void | Promise<void>;
|
|
282
|
+
readonly onStepComplete?: (event: StepCompleteEvent) => void | Promise<void>;
|
|
283
|
+
readonly onStepError?: (event: StepErrorEvent) => void | Promise<void>;
|
|
284
|
+
readonly onStepRetry?: (event: StepRetryEvent) => void | Promise<void>;
|
|
285
|
+
readonly onSignalSent?: (event: SignalSentEvent) => void | Promise<void>;
|
|
286
|
+
readonly onSignalReceived?: (event: SignalReceivedEvent) => void | Promise<void>;
|
|
287
|
+
}
|
|
288
|
+
interface WorkerConfig {
|
|
289
|
+
readonly concurrency?: number;
|
|
290
|
+
readonly pollIntervalMs?: Millis;
|
|
291
|
+
/** AbortSignal drives graceful drain on SIGTERM/SIGINT. */
|
|
292
|
+
readonly signal?: AbortSignal;
|
|
293
|
+
}
|
|
294
|
+
interface WorkerRunOnceOpts {
|
|
295
|
+
readonly maxSteps?: number;
|
|
296
|
+
}
|
|
297
|
+
interface WorkerRunUntilEmptyOpts {
|
|
298
|
+
/** Wall-clock deadline in `Date.now()` units. Edge runtimes have CPU/wall-time budgets. */
|
|
299
|
+
readonly deadline?: number;
|
|
300
|
+
}
|
|
301
|
+
interface WorkerRunResult {
|
|
302
|
+
readonly processed: number;
|
|
303
|
+
}
|
|
304
|
+
interface Worker {
|
|
305
|
+
/** Long-running fleet (Cloud Run service, ECS, dedicated VM). Blocks until `signal` aborts. */
|
|
306
|
+
run(): Promise<void>;
|
|
307
|
+
/** HTTP server alongside requests. Returns after up to `maxSteps` are processed. */
|
|
308
|
+
runOnce(opts?: WorkerRunOnceOpts): Promise<WorkerRunResult>;
|
|
309
|
+
/** Serverless / edge. Drains everything available within the deadline and exits. */
|
|
310
|
+
runUntilEmpty(opts?: WorkerRunUntilEmptyOpts): Promise<WorkerRunResult>;
|
|
311
|
+
}
|
|
312
|
+
type ClaimToken = string & {
|
|
313
|
+
readonly __brand: "ClaimToken";
|
|
314
|
+
};
|
|
315
|
+
interface SerializedError {
|
|
316
|
+
readonly name: string;
|
|
317
|
+
readonly message: string;
|
|
318
|
+
readonly stack?: string;
|
|
319
|
+
readonly cause?: Json;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Fact-ordering invariant (must hold for every Store implementation):
|
|
323
|
+
* facts persisted via `appendFact` / `completeStep` / `failStep` MUST be
|
|
324
|
+
* visible to the next `loadRunState` call. The scheduler decides what to run
|
|
325
|
+
* by projecting `RunState.steps` from the fact log, then calls `nextRunnable`
|
|
326
|
+
* — if a `step.completed` write isn't reflected in a subsequent read, the
|
|
327
|
+
* scheduler will re-enqueue or skip steps incorrectly. Adapters with read
|
|
328
|
+
* replicas must route `loadRunState` to the primary or wait for replication
|
|
329
|
+
* within a single `advance` cycle.
|
|
330
|
+
*/
|
|
331
|
+
interface Store {
|
|
332
|
+
appendFact(runId: RunId, fact: Fact): Promise<void>;
|
|
333
|
+
loadRunState(runId: RunId): Promise<RunState>;
|
|
334
|
+
/**
|
|
335
|
+
* Atomically begin a run. Inserts the `flow.started` fact and any
|
|
336
|
+
* materialized run row keyed by `runId` IFF no run with this `runId` already
|
|
337
|
+
* exists. Returns `{ started: true }` if this call created the run,
|
|
338
|
+
* `{ started: false }` if the run already exists.
|
|
339
|
+
*
|
|
340
|
+
* Two concurrent `tryStartRun(runId, ...)` calls with the same `runId` must
|
|
341
|
+
* result in exactly one `flow.started` fact. Adapters enforce this at the
|
|
342
|
+
* durability layer (e.g. Postgres `INSERT ... ON CONFLICT DO NOTHING` keyed
|
|
343
|
+
* by `run_id`), NOT via app-level check-then-insert.
|
|
344
|
+
*
|
|
345
|
+
* The runtime calls this exactly once at `wf.start()`. Subsequent facts go
|
|
346
|
+
* through `appendFact` as usual.
|
|
347
|
+
*/
|
|
348
|
+
tryStartRun(runId: RunId, fact: FlowStartedFact): Promise<{
|
|
349
|
+
readonly started: boolean;
|
|
350
|
+
}>;
|
|
351
|
+
/**
|
|
352
|
+
* Returns null if the step is already claimed under a live lease. Lease
|
|
353
|
+
* duration is owned by the Store implementation; configure it on the adapter.
|
|
354
|
+
*/
|
|
355
|
+
claimStep(runId: RunId, stepId: StepId, attempt: AttemptNumber): Promise<ClaimToken | null>;
|
|
356
|
+
completeStep(runId: RunId, stepId: StepId, output: Json, fact: Fact): Promise<void>;
|
|
357
|
+
failStep(runId: RunId, stepId: StepId, error: SerializedError, fact: Fact): Promise<void>;
|
|
358
|
+
/** Memoization read — returns the persisted output of a previously completed step. */
|
|
359
|
+
getStepOutput(runId: RunId, stepId: StepId): Promise<Json | null>;
|
|
360
|
+
/** `ctx.once` durable record. */
|
|
361
|
+
recordOnce(runId: RunId, stepId: StepId, scope: string, value: Json): Promise<void>;
|
|
362
|
+
getOnce(runId: RunId, stepId: StepId, scope: string): Promise<Json | null>;
|
|
363
|
+
/**
|
|
364
|
+
* Run a task step's handler inside an adapter-owned transaction.
|
|
365
|
+
*
|
|
366
|
+
* Implementations open a transaction (or equivalent atomic scope), invoke
|
|
367
|
+
* `body(tx)`, and on a successful return persist the returned fact
|
|
368
|
+
* atomically with any writes the handler made via `ctx.tx`. The output is
|
|
369
|
+
* returned to the caller; for `step.failed` facts the output is ignored
|
|
370
|
+
* (callers should still return the placeholder shape the type requires).
|
|
371
|
+
*
|
|
372
|
+
* Contract:
|
|
373
|
+
* - If `body` throws, the transaction is rolled back and the error
|
|
374
|
+
* propagates. The caller (dispatcher) is responsible for recording the
|
|
375
|
+
* failure via `failStep` in a separate scope so the failure survives
|
|
376
|
+
* even when domain writes do not.
|
|
377
|
+
* - On a returned `step.completed` fact, the same atomic scope must also
|
|
378
|
+
* record the step output (such that `getStepOutput` reflects it) and
|
|
379
|
+
* release any worker lease held for `(runId, stepId, attempt)`.
|
|
380
|
+
*
|
|
381
|
+
* For adapters with no real transaction (in-memory, sqlite without WAL),
|
|
382
|
+
* `tx` is passed as `undefined as Tx` and the write is non-atomic — those
|
|
383
|
+
* adapters are not used with `ctx.tx` writes.
|
|
384
|
+
*/
|
|
385
|
+
runStep<T extends Json>(runId: RunId, stepId: StepId, attempt: AttemptNumber, body: (tx: Tx) => Promise<{
|
|
386
|
+
readonly output: T;
|
|
387
|
+
readonly fact: StepCompletedFact | StepFailedFact;
|
|
388
|
+
}>): Promise<T>;
|
|
389
|
+
}
|
|
390
|
+
interface QueueMessage {
|
|
391
|
+
readonly receipt: string;
|
|
392
|
+
readonly runId: RunId;
|
|
393
|
+
readonly stepId: StepId;
|
|
394
|
+
readonly payload: Json;
|
|
395
|
+
readonly attempt: AttemptNumber;
|
|
396
|
+
}
|
|
397
|
+
interface QueueDequeueOpts {
|
|
398
|
+
readonly count: number;
|
|
399
|
+
}
|
|
400
|
+
interface QueueEnqueueOpts {
|
|
401
|
+
/** Attempt number to publish. Defaults to 1. The dispatcher controls increments. */
|
|
402
|
+
readonly attempt?: AttemptNumber;
|
|
403
|
+
/** Visibility delay — the message becomes dequeueable after `delayMs`. */
|
|
404
|
+
readonly delayMs?: Millis;
|
|
405
|
+
/** Adapter-specific extra data. Core never reads this. */
|
|
406
|
+
readonly payload?: Json;
|
|
407
|
+
}
|
|
408
|
+
interface Queue {
|
|
409
|
+
enqueue(runId: RunId, stepId: StepId, opts?: QueueEnqueueOpts): Promise<void>;
|
|
410
|
+
dequeue(opts: QueueDequeueOpts): Promise<readonly QueueMessage[]>;
|
|
411
|
+
ack(receipt: string): Promise<void>;
|
|
412
|
+
nack(receipt: string, opts?: {
|
|
413
|
+
delayMs?: Millis;
|
|
414
|
+
}): Promise<void>;
|
|
415
|
+
extend(receipt: string, leaseMs: Millis): Promise<void>;
|
|
416
|
+
}
|
|
417
|
+
interface Clock {
|
|
418
|
+
now(): Date;
|
|
419
|
+
/** Resolves when `ms` has elapsed or `signal` aborts. */
|
|
420
|
+
sleep(ms: Millis, signal?: AbortSignal): Promise<void>;
|
|
421
|
+
/** Persistent timer — wakes the scheduler at `at` for `(runId, stepId)`. */
|
|
422
|
+
schedule(at: Date, runId: RunId, stepId: StepId): Promise<void>;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Wakes the scheduler when there's work to do. Default impl polls via the
|
|
426
|
+
* Queue plugin; Postgres adapter offers a NOTIFY/LISTEN variant.
|
|
427
|
+
* Per the sharding-safety lock: the handler receives a `runId` — there is no
|
|
428
|
+
* global tick.
|
|
429
|
+
*/
|
|
430
|
+
interface Trigger {
|
|
431
|
+
subscribe(handler: (runId: RunId) => void): () => void;
|
|
432
|
+
}
|
|
433
|
+
type FactKind = "flow.started" | "flow.completed" | "flow.failed" | "step.started" | "step.completed" | "step.failed" | "step.retried" | "step.skipped" | "signal.sent" | "signal.received" | "once.recorded" | "match.arm-selected";
|
|
434
|
+
interface FactBase {
|
|
435
|
+
readonly runId: RunId;
|
|
436
|
+
readonly at: Date;
|
|
437
|
+
}
|
|
438
|
+
interface FlowStartedFact extends FactBase {
|
|
439
|
+
readonly kind: "flow.started";
|
|
440
|
+
readonly flowId: string;
|
|
441
|
+
readonly input: Json;
|
|
442
|
+
}
|
|
443
|
+
interface FlowCompletedFact extends FactBase {
|
|
444
|
+
readonly kind: "flow.completed";
|
|
445
|
+
readonly output: Json;
|
|
446
|
+
}
|
|
447
|
+
interface FlowFailedFact extends FactBase {
|
|
448
|
+
readonly kind: "flow.failed";
|
|
449
|
+
readonly error: SerializedError;
|
|
450
|
+
}
|
|
451
|
+
interface StepStartedFact extends FactBase {
|
|
452
|
+
readonly kind: "step.started";
|
|
453
|
+
readonly stepId: StepId;
|
|
454
|
+
readonly attempt: AttemptNumber;
|
|
455
|
+
}
|
|
456
|
+
interface StepCompletedFact extends FactBase {
|
|
457
|
+
readonly kind: "step.completed";
|
|
458
|
+
readonly stepId: StepId;
|
|
459
|
+
readonly attempt: AttemptNumber;
|
|
460
|
+
readonly output: Json;
|
|
461
|
+
}
|
|
462
|
+
interface StepFailedFact extends FactBase {
|
|
463
|
+
readonly kind: "step.failed";
|
|
464
|
+
readonly stepId: StepId;
|
|
465
|
+
readonly attempt: AttemptNumber;
|
|
466
|
+
readonly error: SerializedError;
|
|
467
|
+
}
|
|
468
|
+
interface StepRetriedFact extends FactBase {
|
|
469
|
+
readonly kind: "step.retried";
|
|
470
|
+
readonly stepId: StepId;
|
|
471
|
+
readonly attempt: AttemptNumber;
|
|
472
|
+
readonly nextAttemptAt: Date;
|
|
473
|
+
}
|
|
474
|
+
interface StepSkippedFact extends FactBase {
|
|
475
|
+
readonly kind: "step.skipped";
|
|
476
|
+
readonly stepId: StepId;
|
|
477
|
+
readonly reason: "when-false" | "transitive";
|
|
478
|
+
}
|
|
479
|
+
interface SignalSentFact extends FactBase {
|
|
480
|
+
readonly kind: "signal.sent";
|
|
481
|
+
readonly stepId: StepId;
|
|
482
|
+
readonly payload: Json;
|
|
483
|
+
}
|
|
484
|
+
interface SignalReceivedFact extends FactBase {
|
|
485
|
+
readonly kind: "signal.received";
|
|
486
|
+
readonly stepId: StepId;
|
|
487
|
+
readonly payload: Json;
|
|
488
|
+
}
|
|
489
|
+
interface OnceRecordedFact extends FactBase {
|
|
490
|
+
readonly kind: "once.recorded";
|
|
491
|
+
readonly stepId: StepId;
|
|
492
|
+
readonly scope: string;
|
|
493
|
+
readonly value: Json;
|
|
494
|
+
}
|
|
495
|
+
interface MatchArmSelectedFact extends FactBase {
|
|
496
|
+
readonly kind: "match.arm-selected";
|
|
497
|
+
readonly stepId: StepId;
|
|
498
|
+
readonly arm: string;
|
|
499
|
+
}
|
|
500
|
+
type Fact = FlowStartedFact | FlowCompletedFact | FlowFailedFact | StepStartedFact | StepCompletedFact | StepFailedFact | StepRetriedFact | StepSkippedFact | SignalSentFact | SignalReceivedFact | OnceRecordedFact | MatchArmSelectedFact;
|
|
501
|
+
type RunStatus = "pending" | "running" | "completed" | "failed";
|
|
502
|
+
type StepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
|
|
503
|
+
interface StepState {
|
|
504
|
+
readonly stepId: StepId;
|
|
505
|
+
readonly status: StepStatus;
|
|
506
|
+
readonly attempts: AttemptNumber;
|
|
507
|
+
readonly output?: Json;
|
|
508
|
+
readonly error?: SerializedError;
|
|
509
|
+
}
|
|
510
|
+
interface RunState {
|
|
511
|
+
readonly runId: RunId;
|
|
512
|
+
readonly flowId: string;
|
|
513
|
+
readonly status: RunStatus;
|
|
514
|
+
readonly steps: Readonly<Record<StepId, StepState>>;
|
|
515
|
+
readonly facts: ReadonlyArray<Fact>;
|
|
516
|
+
}
|
|
517
|
+
type ReplayMode = "inspect" | "continue";
|
|
518
|
+
interface ReplayOpts {
|
|
519
|
+
readonly mode: ReplayMode;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Construct a flow.
|
|
524
|
+
*
|
|
525
|
+
* 1. Run `build(b)` to collect the user's StepMap. Each call to `b.task` /
|
|
526
|
+
* `b.signal` / `b.match` produced a Step with `id: ""` and a captured def
|
|
527
|
+
* (which may reference upstream Step values from earlier in the closure).
|
|
528
|
+
* Match defs additionally hold their arms' nested StepMaps on `_nested`.
|
|
529
|
+
* 2. Walk the returned map *recursively*, descending into match arms. Each
|
|
530
|
+
* step gets an id derived from its position: top-level keys verbatim,
|
|
531
|
+
* nested arm steps namespaced as `<matchKey>.<armId>.<stepKey>`.
|
|
532
|
+
* 3. Rewrite every def's `needs` so each upstream Step value carries its
|
|
533
|
+
* assigned id. Nested-arm step defs additionally get a `parentMatch`
|
|
534
|
+
* annotation so the scheduler can gate them on arm selection.
|
|
535
|
+
* 4. Promote `MatchArmDef._nested` → `MatchArmDef.stepIds` (the namespaced
|
|
536
|
+
* IDs of the arm's nested steps), then drop `_nested`.
|
|
537
|
+
*/
|
|
538
|
+
declare function flow<const Id extends string, InputSchema extends StandardSchemaV1, M extends StepMap, Output = unknown>(config: FlowConfig<Id, InputSchema, M, Output>): Flow<Id, InputSchema, M, Output>;
|
|
539
|
+
|
|
540
|
+
interface InMemoryStoreOpts {
|
|
541
|
+
readonly leaseMs?: Millis;
|
|
542
|
+
}
|
|
543
|
+
declare class InMemoryStore implements Store {
|
|
544
|
+
private readonly facts;
|
|
545
|
+
private readonly outputs;
|
|
546
|
+
private readonly onces;
|
|
547
|
+
private readonly leases;
|
|
548
|
+
private readonly leaseMs;
|
|
549
|
+
constructor(opts?: InMemoryStoreOpts);
|
|
550
|
+
appendFact(runId: RunId, fact: Fact): Promise<void>;
|
|
551
|
+
tryStartRun(runId: RunId, fact: FlowStartedFact): Promise<{
|
|
552
|
+
readonly started: boolean;
|
|
553
|
+
}>;
|
|
554
|
+
loadRunState(runId: RunId): Promise<RunState>;
|
|
555
|
+
claimStep(runId: RunId, stepId: StepId, attempt: AttemptNumber): Promise<ClaimToken | null>;
|
|
556
|
+
completeStep(runId: RunId, stepId: StepId, output: Json, fact: Fact): Promise<void>;
|
|
557
|
+
failStep(runId: RunId, _stepId: StepId, _error: SerializedError, fact: Fact): Promise<void>;
|
|
558
|
+
getStepOutput(runId: RunId, stepId: StepId): Promise<Json | null>;
|
|
559
|
+
recordOnce(runId: RunId, stepId: StepId, scope: string, value: Json): Promise<void>;
|
|
560
|
+
getOnce(runId: RunId, stepId: StepId, scope: string): Promise<Json | null>;
|
|
561
|
+
runStep<T extends Json>(runId: RunId, stepId: StepId, _attempt: AttemptNumber, body: (tx: Tx) => Promise<{
|
|
562
|
+
readonly output: T;
|
|
563
|
+
readonly fact: StepCompletedFact | StepFailedFact;
|
|
564
|
+
}>): Promise<T>;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Project an append-only fact stream into a `RunState`. Public so adapters
|
|
568
|
+
* (e.g. `@nagi-js/postgres`) can re-use the same projection rules — keeping
|
|
569
|
+
* one canonical definition of "what does the fact log mean".
|
|
570
|
+
*/
|
|
571
|
+
declare function projectRunState(runId: RunId, facts: readonly Fact[]): RunState;
|
|
572
|
+
interface InMemoryQueueOpts {
|
|
573
|
+
readonly leaseMs?: Millis;
|
|
574
|
+
}
|
|
575
|
+
declare class InMemoryQueue implements Queue {
|
|
576
|
+
private readonly pending;
|
|
577
|
+
private readonly leased;
|
|
578
|
+
private readonly leaseMs;
|
|
579
|
+
constructor(opts?: InMemoryQueueOpts);
|
|
580
|
+
enqueue(runId: RunId, stepId: StepId, opts?: QueueEnqueueOpts): Promise<void>;
|
|
581
|
+
dequeue(opts: QueueDequeueOpts): Promise<readonly QueueMessage[]>;
|
|
582
|
+
ack(receipt: string): Promise<void>;
|
|
583
|
+
nack(receipt: string, opts?: {
|
|
584
|
+
delayMs?: Millis;
|
|
585
|
+
}): Promise<void>;
|
|
586
|
+
extend(receipt: string, leaseMs: Millis): Promise<void>;
|
|
587
|
+
}
|
|
588
|
+
interface InMemoryClockOpts {
|
|
589
|
+
/**
|
|
590
|
+
* Wires `schedule()` wake-ups to a trigger. When the persistent timer fires,
|
|
591
|
+
* the clock calls `trigger.fire(runId)` so scheduler subscribers can resume
|
|
592
|
+
* the run. Without this, `schedule()` is a no-op on the worker loop and any
|
|
593
|
+
* step that depends on it (time-based gates, long-delayed retries) will
|
|
594
|
+
* stall in tests.
|
|
595
|
+
*/
|
|
596
|
+
readonly trigger?: InMemoryTrigger;
|
|
597
|
+
}
|
|
598
|
+
declare class InMemoryClock implements Clock {
|
|
599
|
+
private readonly timers;
|
|
600
|
+
private readonly trigger;
|
|
601
|
+
constructor(opts?: InMemoryClockOpts);
|
|
602
|
+
now(): Date;
|
|
603
|
+
sleep(ms: Millis, signal?: AbortSignal): Promise<void>;
|
|
604
|
+
schedule(at: Date, runId: RunId, stepId: StepId): Promise<void>;
|
|
605
|
+
/** Clear any pending scheduled timers. Call from test teardown. */
|
|
606
|
+
dispose(): void;
|
|
607
|
+
}
|
|
608
|
+
declare class InMemoryTrigger implements Trigger {
|
|
609
|
+
private handlers;
|
|
610
|
+
subscribe(handler: (runId: RunId) => void): () => void;
|
|
611
|
+
/**
|
|
612
|
+
* Test-only: synchronously dispatch a wake-up to all subscribers.
|
|
613
|
+
* Real triggers fire from a Store change (NOTIFY/LISTEN, polling, etc.).
|
|
614
|
+
*/
|
|
615
|
+
fire(runId: RunId): void;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
interface NagiConfig {
|
|
619
|
+
readonly flows: ReadonlyArray<Flow>;
|
|
620
|
+
readonly store: Store;
|
|
621
|
+
readonly queue: Queue;
|
|
622
|
+
readonly clock?: Clock;
|
|
623
|
+
readonly trigger?: Trigger;
|
|
624
|
+
readonly hooks?: FlowHooks;
|
|
625
|
+
readonly logger?: Logger;
|
|
626
|
+
readonly defaultRetry?: RetryPolicy;
|
|
627
|
+
}
|
|
628
|
+
interface StartOpts {
|
|
629
|
+
/**
|
|
630
|
+
* Caller-supplied runId for idempotent kickoff. If provided and a run with
|
|
631
|
+
* this ID already exists, `start()` is a no-op and returns the same ID
|
|
632
|
+
* without re-appending `flow.started`, re-dispatching, or re-validating the
|
|
633
|
+
* input. Two concurrent `start()` calls with the same `runId` produce
|
|
634
|
+
* exactly one run (enforced at the Store layer).
|
|
635
|
+
*
|
|
636
|
+
* If omitted, the runtime mints a fresh ID via `crypto.randomUUID()`.
|
|
637
|
+
*
|
|
638
|
+
* Typical usage: a content hash of the input, so callers de-duplicate
|
|
639
|
+
* kickoffs without coordinating.
|
|
640
|
+
*/
|
|
641
|
+
readonly runId?: RunId;
|
|
642
|
+
}
|
|
643
|
+
interface Wf {
|
|
644
|
+
/** Begin a new run. Returns the runId. */
|
|
645
|
+
start<F extends Flow>(flow: F, input: FlowInput<F>, opts?: StartOpts): Promise<RunId>;
|
|
646
|
+
/** Resolve a `b.signal()` step waiting on external input. */
|
|
647
|
+
signal(runId: RunId, stepName: string, payload: unknown): Promise<void>;
|
|
648
|
+
/** Construct a worker; call `run` / `runOnce` / `runUntilEmpty` on it. */
|
|
649
|
+
worker(config?: WorkerConfig): Worker;
|
|
650
|
+
/**
|
|
651
|
+
* Re-dispatch from the first incomplete step. `mode: "continue"` runs side
|
|
652
|
+
* effects (idempotency protects); `mode: "inspect"` is a no-op probe.
|
|
653
|
+
*/
|
|
654
|
+
replay(runId: RunId, opts?: ReplayOpts): Promise<void>;
|
|
655
|
+
}
|
|
656
|
+
declare class NagiValidationError extends Error {
|
|
657
|
+
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
658
|
+
constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
659
|
+
}
|
|
660
|
+
declare class NagiRuntimeError extends Error {
|
|
661
|
+
constructor(message: string);
|
|
662
|
+
}
|
|
663
|
+
declare function nagi(config: NagiConfig): Wf;
|
|
664
|
+
|
|
665
|
+
export { type AttemptNumber, type BackoffStrategy, type Builder, type ClaimToken, type Clock, type Fact, type FactKind, type Flow, type FlowCompleteEvent, type FlowCompletedFact, type FlowConfig, type FlowErrorEvent, type FlowEvent, type FlowFailedFact, type FlowHooks, type FlowInput, type FlowOutput, type FlowStartEvent, type FlowStartedFact, InMemoryClock, InMemoryQueue, InMemoryStore, InMemoryTrigger, type InferSchemaInput, type InferSchemaOutput, type Json, type Logger, type MatchArm, type MatchArmGuard, type MatchArmOtherwise, type MatchArmOutput, type MatchArmSelectedFact, type MatchArmShape, type MatchDiscriminatorConfig, type MatchDiscriminatorOutput, type MatchGuardConfig, type Millis, type NagiConfig, NagiRuntimeError, NagiValidationError, type NeedsMap, type NeedsOutputs, type OnceRecordedFact, type Queue, type QueueDequeueOpts, type QueueEnqueueOpts, type QueueMessage, type Register, type ReplayMode, type ReplayOpts, type RetryPolicy, type RunId, type RunState, type RunStatus, type SerializedError, type SignalConfig, type SignalReceivedEvent, type SignalReceivedFact, type SignalSentEvent, type SignalSentFact, StandardSchemaV1, type StartOpts, type Step, type StepCompleteEvent, type StepCompletedFact, type StepCtx, type StepErrorEvent, type StepEvent, type StepFailedFact, type StepId, type StepKind, type StepMap, type StepOutput, type StepRetriedFact, type StepRetryEvent, type StepSkippedFact, type StepStartEvent, type StepStartedFact, type StepState, type StepStatus, type Store, type TaskConfig, type Trigger, type Tx, type Wf, type Worker, type WorkerConfig, type WorkerRunOnceOpts, type WorkerRunResult, type WorkerRunUntilEmptyOpts, flow, nagi, projectRunState };
|