flowneer 0.1.0 → 0.3.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/README.md +242 -19
- package/dist/index.d.ts +31 -3
- package/dist/index.js +99 -19
- package/dist/plugins/dev/index.d.ts +21 -1
- package/dist/plugins/dev/index.js +27 -1
- package/dist/plugins/index.d.ts +5 -4
- package/dist/plugins/index.js +216 -11
- package/dist/plugins/messaging/index.d.ts +56 -0
- package/dist/plugins/messaging/index.js +57 -0
- package/dist/plugins/observability/index.d.ts +16 -1
- package/dist/plugins/observability/index.js +24 -0
- package/dist/plugins/persistence/index.d.ts +37 -1
- package/dist/plugins/persistence/index.js +64 -1
- package/dist/plugins/resilience/index.d.ts +13 -1
- package/dist/plugins/resilience/index.js +46 -10
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephan Langeveld
|
|
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/README.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Flowneer
|
|
2
2
|
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://www.npmjs.com/package/flowneer"><img src="https://badges.ws/npm/v/flowneer" /></a>
|
|
5
|
+
<a href="https://deno.bundlejs.com/badge?q=flowneer"><img src="https://deno.bundlejs.com/badge?q=flowneer" /></a>
|
|
6
|
+
<a href="https://www.npmjs.com/package/flowneer"><img src="https://badges.ws/npm/l/flowneer" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/flowneer"><img src="https://badges.ws/npm/dt/flowneer" /></a>
|
|
8
|
+
<a href="https://deepwiki.com/Fanna1119/flowneer"><img src="https://deepwiki.com/badge.svg" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
3
11
|
A tiny, zero-dependency fluent flow builder for TypeScript. Chain steps, branch on conditions, loop, batch-process, and run tasks in parallel — all through a single `FlowBuilder` class. Extend it with plugins.
|
|
4
12
|
|
|
5
13
|
## Install
|
|
@@ -121,7 +129,7 @@ await new FlowBuilder<SumState>()
|
|
|
121
129
|
// → [2, 4, 6]
|
|
122
130
|
```
|
|
123
131
|
|
|
124
|
-
### `parallel(fns, options?)`
|
|
132
|
+
### `parallel(fns, options?, reducer?)`
|
|
125
133
|
|
|
126
134
|
Run multiple functions concurrently against the same shared state.
|
|
127
135
|
|
|
@@ -155,6 +163,67 @@ await new FlowBuilder<FetchState>()
|
|
|
155
163
|
// → Fetched 100 posts and 10 users
|
|
156
164
|
```
|
|
157
165
|
|
|
166
|
+
When a `reducer` is provided each fn receives its own **shallow clone** of `shared`, preventing concurrent write races. After all fns settle the reducer merges the drafts back into the original:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface ScoreState {
|
|
170
|
+
value: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await new FlowBuilder<ScoreState>()
|
|
174
|
+
.parallel(
|
|
175
|
+
[
|
|
176
|
+
async (s) => {
|
|
177
|
+
s.value += 10;
|
|
178
|
+
},
|
|
179
|
+
async (s) => {
|
|
180
|
+
s.value += 20;
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
undefined,
|
|
184
|
+
(original, drafts) => {
|
|
185
|
+
// drafts[0].value === 10, drafts[1].value === 20 (each started at 0)
|
|
186
|
+
original.value = drafts.reduce((sum, d) => sum + d.value, 0);
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
.run({ value: 0 });
|
|
190
|
+
// original.value === 30
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
See [`withAtomicUpdates`](#withatomicupdates) for the plugin shorthand.
|
|
194
|
+
|
|
195
|
+
### `label(name)`
|
|
196
|
+
|
|
197
|
+
Insert a named marker in the step chain. Labels are no-ops during normal execution — they exist only as jump targets.
|
|
198
|
+
|
|
199
|
+
Any `NodeFn` can return `"→labelName"` to jump back (or forward) to that label, enabling iterative refinement and reflection loops without nesting:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
interface RefineState {
|
|
203
|
+
draft: string;
|
|
204
|
+
passes: number;
|
|
205
|
+
quality: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await new FlowBuilder<RefineState>()
|
|
209
|
+
.startWith(async (s) => {
|
|
210
|
+
s.draft = await generateDraft(s);
|
|
211
|
+
})
|
|
212
|
+
.label("refine")
|
|
213
|
+
.then(async (s) => {
|
|
214
|
+
s.quality = await scoreDraft(s.draft);
|
|
215
|
+
if (s.quality < 0.8) {
|
|
216
|
+
s.draft = await improveDraft(s.draft);
|
|
217
|
+
s.passes++;
|
|
218
|
+
return "→refine"; // jump back to the label
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
.then(async (s) => console.log("Final draft after", s.passes, "passes"))
|
|
222
|
+
.run({ draft: "", passes: 0, quality: 0 });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> **Tip:** Pair with [`withCycles`](#withcycles) to cap the maximum number of jumps.
|
|
226
|
+
|
|
158
227
|
### `run(shared, params?, options?)`
|
|
159
228
|
|
|
160
229
|
Execute the flow. Optionally pass a `params` object that every step receives as a second argument.
|
|
@@ -210,12 +279,89 @@ FlowError: Flow failed at loop (step 1): exploded on tick 2
|
|
|
210
279
|
FlowError: Flow failed at batch (step 0): bad item: 3
|
|
211
280
|
```
|
|
212
281
|
|
|
282
|
+
### `InterruptError`
|
|
283
|
+
|
|
284
|
+
`InterruptError` is a special error that **bypasses `FlowError` wrapping** — it propagates directly to the caller. Use it for human-in-the-loop and approval patterns (via [`withInterrupts`](#withinterrupts)).
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { FlowBuilder, InterruptError } from "flowneer";
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await flow.run(shared);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (err instanceof InterruptError) {
|
|
293
|
+
// err.savedShared is a deep clone of state at the interrupt point
|
|
294
|
+
const approval = await askHuman(err.savedShared);
|
|
295
|
+
if (approval) await flow.run(shared); // resume from scratch or use withReplay
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
213
300
|
## Plugins
|
|
214
301
|
|
|
215
302
|
The core is intentionally small. Use `FlowBuilder.use(plugin)` to add chain methods.
|
|
216
303
|
|
|
217
304
|
A plugin is an object of functions that get copied onto `FlowBuilder.prototype`. Each function receives the builder as `this` and should return `this` for chaining.
|
|
218
305
|
|
|
306
|
+
### Available plugins
|
|
307
|
+
|
|
308
|
+
| Category | Plugin | Method | Description |
|
|
309
|
+
| ----------------- | ------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
310
|
+
| **Observability** | `withHistory` | `.withHistory()` | Appends a shallow state snapshot after each step to `shared.__history` |
|
|
311
|
+
| | `withTiming` | `.withTiming()` | Records wall-clock duration (ms) of each step in `shared.__timings[index]` |
|
|
312
|
+
| | `withVerbose` | `.withVerbose()` | Prints the full `shared` object to stdout after each step |
|
|
313
|
+
| | `withInterrupts` | `.interruptIf(condition)` | Pauses the flow by throwing an `InterruptError` (with a deep-clone of `shared`) when condition is true |
|
|
314
|
+
| **Persistence** | `withCheckpoint` | `.withCheckpoint(store)` | Saves `shared` to a store after each successful step |
|
|
315
|
+
| | `withAuditLog` | `.withAuditLog(store)` | Writes an immutable deep-clone audit entry to a store after every step (success and error) |
|
|
316
|
+
| | `withReplay` | `.withReplay(fromStep)` | Skips all steps before `fromStep`; combine with `.withCheckpoint()` to resume a failed flow |
|
|
317
|
+
| | `withVersionedCheckpoint` | `.withVersionedCheckpoint(store)` | Saves diff-based versioned checkpoints with parent pointers after each step that changes state |
|
|
318
|
+
| | | `.resumeFrom(version, store)` | Resolves a version id and skips all steps up to and including the saved step index |
|
|
319
|
+
| **Resilience** | `withCircuitBreaker` | `.withCircuitBreaker(opts?)` | Opens the circuit after `maxFailures` consecutive failures and rejects all steps until `resetMs` elapses |
|
|
320
|
+
| | `withFallback` | `.withFallback(fn)` | Catches any step error and calls `fn` instead of propagating, allowing the flow to continue |
|
|
321
|
+
| | `withTimeout` | `.withTimeout(ms)` | Aborts any step that exceeds `ms` milliseconds with a descriptive error |
|
|
322
|
+
| | `withCycles` | `.withCycles(maxJumps?)` | Throws if total step executions exceed `maxJumps` (default 100) — guards against infinite goto loops |
|
|
323
|
+
| **Messaging** | `withChannels` | `.withChannels()` | Initialises a `Map`-based message-channel system on `shared.__channels` |
|
|
324
|
+
| **LLM** | `withCostTracker` | `.withCostTracker()` | Accumulates per-step `shared.__stepCost` values into `shared.__cost` after each step |
|
|
325
|
+
| | `withRateLimit` | `.withRateLimit({ intervalMs })` | Enforces a minimum gap of `intervalMs` ms between steps to avoid hammering rate-limited APIs |
|
|
326
|
+
| | `withTokenBudget` | `.withTokenBudget(limit)` | Aborts the flow before any step where `shared.tokensUsed >= limit` |
|
|
327
|
+
| **Dev** | `withDryRun` | `.withDryRun()` | Skips all step bodies while still firing hooks — useful for validating observability wiring |
|
|
328
|
+
| | `withMocks` | `.withMocks(map)` | Replaces step bodies at specified indices with mock functions; all other steps run normally |
|
|
329
|
+
| | `withStepLimit` | `.withStepLimit(max?)` | Throws after `max` total step executions (default 1000); counter resets on each `run()` call |
|
|
330
|
+
| | `withAtomicUpdates` | `.parallelAtomic(fns, reducer, options?)` | Sugar over `parallel()` with a reducer — each fn runs on an isolated draft, reducer merges results |
|
|
331
|
+
|
|
332
|
+
Plugins are imported from `flowneer/plugins` (or their individual subpath) and registered once with `FlowBuilder.use()`:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { withTiming, withCostTracker } from "flowneer/plugins";
|
|
336
|
+
|
|
337
|
+
FlowBuilder.use(withTiming);
|
|
338
|
+
FlowBuilder.use(withCostTracker);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Messaging utilities are standalone functions — no need to register them as a plugin method:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import {
|
|
345
|
+
withChannels,
|
|
346
|
+
sendTo,
|
|
347
|
+
receiveFrom,
|
|
348
|
+
peekChannel,
|
|
349
|
+
} from "flowneer/plugins/messaging";
|
|
350
|
+
|
|
351
|
+
FlowBuilder.use(withChannels);
|
|
352
|
+
|
|
353
|
+
const flow = new FlowBuilder()
|
|
354
|
+
.withChannels()
|
|
355
|
+
.startWith(async (s) => {
|
|
356
|
+
sendTo(s, "results", { score: 42 });
|
|
357
|
+
})
|
|
358
|
+
.then(async (s) => {
|
|
359
|
+
const msgs = receiveFrom(s, "results"); // [{ score: 42 }]
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
219
365
|
### Writing a plugin
|
|
220
366
|
|
|
221
367
|
```typescript
|
|
@@ -257,23 +403,33 @@ const flow = new FlowBuilder<MyState>()
|
|
|
257
403
|
|
|
258
404
|
### Lifecycle hooks
|
|
259
405
|
|
|
260
|
-
Plugins register hooks via `_setHooks()`.
|
|
406
|
+
Plugins register hooks via `_setHooks()`. The following hook points are available:
|
|
407
|
+
|
|
408
|
+
| Hook | Called | Arguments |
|
|
409
|
+
| ---------------- | --------------------------------------------------------- | --------------------------------------- |
|
|
410
|
+
| `beforeFlow` | Once before the first step | `(shared, params)` |
|
|
411
|
+
| `beforeStep` | Before each step executes | `(meta, shared, params)` |
|
|
412
|
+
| `wrapStep` | Wraps step execution — call `next()` to run the step body | `(meta, next, shared, params)` |
|
|
413
|
+
| `afterStep` | After each step completes | `(meta, shared, params)` |
|
|
414
|
+
| `wrapParallelFn` | Wraps each individual fn inside a `parallel()` step | `(meta, fnIndex, next, shared, params)` |
|
|
415
|
+
| `onError` | When a step throws (before re-throwing) | `(meta, error, shared, params)` |
|
|
416
|
+
| `afterFlow` | After the flow finishes (success or failure) | `(shared, params)` |
|
|
261
417
|
|
|
262
|
-
|
|
263
|
-
| ------------ | -------------------------------------------- | ------------------------------- |
|
|
264
|
-
| `beforeStep` | Before each step executes | `(meta, shared, params)` |
|
|
265
|
-
| `afterStep` | After each step completes | `(meta, shared, params)` |
|
|
266
|
-
| `onError` | When a step throws (before re-throwing) | `(meta, error, shared, params)` |
|
|
267
|
-
| `afterFlow` | After the flow finishes (success or failure) | `(shared, params)` |
|
|
418
|
+
Multiple `wrapStep` (or `wrapParallelFn`) registrations compose — the first registered is the outermost wrapper. Omitting `next()` skips the step body entirely (used by `withDryRun`, `withMocks`, `withReplay`).
|
|
268
419
|
|
|
269
420
|
### What plugins are for
|
|
270
421
|
|
|
271
|
-
| Concern
|
|
272
|
-
|
|
|
273
|
-
| Observability / tracing
|
|
274
|
-
| Persistence / checkpointing
|
|
275
|
-
|
|
|
276
|
-
|
|
|
422
|
+
| Concern | Plugin / hook | Hook(s) used |
|
|
423
|
+
| ---------------------------- | ----------------------------- | ----------------------------------- |
|
|
424
|
+
| Observability / tracing | `withHistory`, `withTiming` | `beforeStep` + `afterStep` |
|
|
425
|
+
| Persistence / checkpointing | `withCheckpoint` | `afterStep` |
|
|
426
|
+
| Versioned persistence | `withVersionedCheckpoint` | `beforeFlow` + `afterStep` |
|
|
427
|
+
| Step/execution skip | `withDryRun`, `withReplay` | `wrapStep` |
|
|
428
|
+
| Safe parallel isolation | `withAtomicUpdates` | `wrapParallelFn` (via core reducer) |
|
|
429
|
+
| Human-in-the-loop / approval | `withInterrupts` | `then()` + `InterruptError` |
|
|
430
|
+
| Message passing | `withChannels` | `beforeFlow` |
|
|
431
|
+
| Infinite-loop protection | `withCycles`, `withStepLimit` | `afterStep` / `beforeStep` |
|
|
432
|
+
| Cleanup / teardown | custom | `afterFlow` |
|
|
277
433
|
|
|
278
434
|
See [examples/observePlugin.ts](examples/observePlugin.ts) and [examples/persistPlugin.ts](examples/persistPlugin.ts) for complete implementations.
|
|
279
435
|
|
|
@@ -380,15 +536,82 @@ const orchestrator = new FlowBuilder<AnalysisState>()
|
|
|
380
536
|
|
|
381
537
|
All three sub-agents share the same `shared` object and run concurrently. Avoid writing to the same key from parallel sub-agents — writes are not synchronised.
|
|
382
538
|
|
|
539
|
+
To eliminate race conditions entirely, pass a `reducer` as the third argument to `.parallel()`, or use `.parallelAtomic()` from [`withAtomicUpdates`](#withatomicupdates). Each sub-agent then operates on its own isolated draft and the reducer decides how to merge.
|
|
540
|
+
|
|
541
|
+
### Iterative refinement with `label` + goto
|
|
542
|
+
|
|
543
|
+
Use `label` / `→label` return values for reflection loops that don't need nesting:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
const reactAgent = new FlowBuilder<AgentState>()
|
|
547
|
+
.startWith(think)
|
|
548
|
+
.label("act")
|
|
549
|
+
.then(async (s) => {
|
|
550
|
+
const result = await callTool(s.toolCall);
|
|
551
|
+
s.observations.push(result);
|
|
552
|
+
s.done = await shouldStop(s);
|
|
553
|
+
if (!s.done) return "→act";
|
|
554
|
+
})
|
|
555
|
+
.then(formatOutput);
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Human-in-the-loop with `interruptIf`
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { withInterrupts, InterruptError } from "flowneer/plugins/observability";
|
|
562
|
+
FlowBuilder.use(withInterrupts);
|
|
563
|
+
|
|
564
|
+
const flow = new FlowBuilder<DraftState>()
|
|
565
|
+
.startWith(generateDraft)
|
|
566
|
+
.interruptIf((s) => s.requiresApproval) // pauses here
|
|
567
|
+
.then(publishDraft);
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
await flow.run(state);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (err instanceof InterruptError) {
|
|
573
|
+
// err.savedShared holds state at the pause point
|
|
574
|
+
await showReviewUI(err.savedShared);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
383
579
|
## Project structure
|
|
384
580
|
|
|
385
581
|
```
|
|
386
|
-
Flowneer.ts
|
|
387
|
-
index.ts
|
|
582
|
+
Flowneer.ts Core — FlowBuilder, FlowError, InterruptError, types
|
|
583
|
+
index.ts Public exports
|
|
584
|
+
plugins/
|
|
585
|
+
observability/
|
|
586
|
+
withHistory.ts State snapshot history
|
|
587
|
+
withTiming.ts Per-step wall-clock timing
|
|
588
|
+
withVerbose.ts Stdout logging
|
|
589
|
+
withInterrupts.ts Human-in-the-loop / approval gates
|
|
590
|
+
persistence/
|
|
591
|
+
withCheckpoint.ts Post-step state saves
|
|
592
|
+
withAuditLog.ts Immutable audit trail
|
|
593
|
+
withReplay.ts Skip-to-step for crash recovery
|
|
594
|
+
withVersionedCheckpoint.ts Diff-based versioned saves + resumeFrom
|
|
595
|
+
resilience/
|
|
596
|
+
withCircuitBreaker.ts
|
|
597
|
+
withFallback.ts
|
|
598
|
+
withTimeout.ts
|
|
599
|
+
withCycles.ts Guard against infinite goto loops
|
|
600
|
+
llm/
|
|
601
|
+
withCostTracker.ts
|
|
602
|
+
withRateLimit.ts
|
|
603
|
+
withTokenBudget.ts
|
|
604
|
+
messaging/
|
|
605
|
+
withChannels.ts Map-based message channels (sendTo / receiveFrom)
|
|
606
|
+
dev/
|
|
607
|
+
withDryRun.ts
|
|
608
|
+
withMocks.ts
|
|
609
|
+
withStepLimit.ts Cap total step executions
|
|
610
|
+
withAtomicUpdates.ts parallelAtomic() shorthand
|
|
388
611
|
examples/
|
|
389
|
-
assistantFlow.ts
|
|
390
|
-
observePlugin.ts
|
|
391
|
-
persistPlugin.ts
|
|
612
|
+
assistantFlow.ts Interactive LLM assistant with branching
|
|
613
|
+
observePlugin.ts Tracing plugin example
|
|
614
|
+
persistPlugin.ts Checkpoint plugin example
|
|
392
615
|
```
|
|
393
616
|
|
|
394
617
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -42,12 +42,18 @@ interface ParallelStep<S, P extends Record<string, unknown>> {
|
|
|
42
42
|
retries: number;
|
|
43
43
|
delaySec: number;
|
|
44
44
|
timeoutMs: number;
|
|
45
|
+
reducer?: (shared: S, drafts: S[]) => void;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
interface LabelStep {
|
|
48
|
+
type: "label";
|
|
49
|
+
name: string;
|
|
50
|
+
}
|
|
51
|
+
type Step<S, P extends Record<string, unknown>> = FnStep<S, P> | BranchStep<S, P> | LoopStep<S, P> | BatchStep<S, P> | ParallelStep<S, P> | LabelStep;
|
|
47
52
|
/** Metadata exposed to hooks — intentionally minimal to avoid coupling. */
|
|
48
53
|
interface StepMeta {
|
|
49
54
|
index: number;
|
|
50
55
|
type: Step<any, any>["type"];
|
|
56
|
+
label?: string;
|
|
51
57
|
}
|
|
52
58
|
/** Lifecycle hooks that plugins can register. */
|
|
53
59
|
interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -61,6 +67,11 @@ interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string,
|
|
|
61
67
|
*/
|
|
62
68
|
wrapStep?: (meta: StepMeta, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
|
|
63
69
|
afterStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Wraps individual functions within a `.parallel()` step.
|
|
72
|
+
* `fnIndex` is the position within the fns array.
|
|
73
|
+
*/
|
|
74
|
+
wrapParallelFn?: (meta: StepMeta, fnIndex: number, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
|
|
64
75
|
onError?: (meta: StepMeta, error: unknown, shared: S, params: P) => void;
|
|
65
76
|
afterFlow?: (shared: S, params: P) => void | Promise<void>;
|
|
66
77
|
}
|
|
@@ -82,6 +93,14 @@ declare class FlowError extends Error {
|
|
|
82
93
|
readonly cause: unknown;
|
|
83
94
|
constructor(step: string, cause: unknown);
|
|
84
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Thrown by `interruptIf` to pause a flow.
|
|
98
|
+
* Catch this in your runner to save `savedShared` and resume later.
|
|
99
|
+
*/
|
|
100
|
+
declare class InterruptError extends Error {
|
|
101
|
+
readonly savedShared: unknown;
|
|
102
|
+
constructor(shared: unknown);
|
|
103
|
+
}
|
|
85
104
|
/**
|
|
86
105
|
* Fluent builder for composable flows.
|
|
87
106
|
*
|
|
@@ -119,8 +138,17 @@ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<st
|
|
|
119
138
|
/**
|
|
120
139
|
* Append a parallel step.
|
|
121
140
|
* Runs all `fns` concurrently against the same shared state.
|
|
141
|
+
*
|
|
142
|
+
* When `reducer` is provided each fn receives its own shallow clone of
|
|
143
|
+
* `shared`; after all fns complete the reducer merges the drafts back
|
|
144
|
+
* into the original shared object — preventing concurrent mutation races.
|
|
145
|
+
*/
|
|
146
|
+
parallel(fns: NodeFn<S, P>[], options?: NodeOptions, reducer?: (shared: S, drafts: S[]) => void): this;
|
|
147
|
+
/**
|
|
148
|
+
* Insert a named label. Labels are no-op markers that can be jumped to
|
|
149
|
+
* from any `NodeFn` by returning `"→labelName"`.
|
|
122
150
|
*/
|
|
123
|
-
|
|
151
|
+
label(name: string): this;
|
|
124
152
|
/** Execute the flow. */
|
|
125
153
|
run(shared: S, params?: P, options?: RunOptions): Promise<void>;
|
|
126
154
|
protected _execute(shared: S, params: P, signal?: AbortSignal): Promise<void>;
|
|
@@ -130,4 +158,4 @@ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<st
|
|
|
130
158
|
private _withTimeout;
|
|
131
159
|
}
|
|
132
160
|
|
|
133
|
-
export { FlowBuilder, FlowError, type FlowHooks, type FlowneerPlugin, type NodeFn, type NodeOptions, type RunOptions, type StepMeta };
|
|
161
|
+
export { FlowBuilder, FlowError, type FlowHooks, type FlowneerPlugin, InterruptError, type NodeFn, type NodeOptions, type RunOptions, type StepMeta };
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,14 @@ var FlowError = class extends Error {
|
|
|
11
11
|
this.cause = cause;
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
|
+
var InterruptError = class extends Error {
|
|
15
|
+
savedShared;
|
|
16
|
+
constructor(shared) {
|
|
17
|
+
super("Flow interrupted");
|
|
18
|
+
this.name = "InterruptError";
|
|
19
|
+
this.savedShared = shared;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
14
22
|
var FlowBuilder = class _FlowBuilder {
|
|
15
23
|
steps = [];
|
|
16
24
|
_hooksList = [];
|
|
@@ -78,10 +86,29 @@ var FlowBuilder = class _FlowBuilder {
|
|
|
78
86
|
/**
|
|
79
87
|
* Append a parallel step.
|
|
80
88
|
* Runs all `fns` concurrently against the same shared state.
|
|
89
|
+
*
|
|
90
|
+
* When `reducer` is provided each fn receives its own shallow clone of
|
|
91
|
+
* `shared`; after all fns complete the reducer merges the drafts back
|
|
92
|
+
* into the original shared object — preventing concurrent mutation races.
|
|
81
93
|
*/
|
|
82
|
-
parallel(fns, options) {
|
|
94
|
+
parallel(fns, options, reducer) {
|
|
83
95
|
const { retries = 1, delaySec = 0, timeoutMs = 0 } = options ?? {};
|
|
84
|
-
this.steps.push({
|
|
96
|
+
this.steps.push({
|
|
97
|
+
type: "parallel",
|
|
98
|
+
fns,
|
|
99
|
+
retries,
|
|
100
|
+
delaySec,
|
|
101
|
+
timeoutMs,
|
|
102
|
+
reducer
|
|
103
|
+
});
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Insert a named label. Labels are no-op markers that can be jumped to
|
|
108
|
+
* from any `NodeFn` by returning `"→labelName"`.
|
|
109
|
+
*/
|
|
110
|
+
label(name) {
|
|
111
|
+
this.steps.push({ type: "label", name });
|
|
85
112
|
return this;
|
|
86
113
|
}
|
|
87
114
|
/** Execute the flow. */
|
|
@@ -98,22 +125,32 @@ var FlowBuilder = class _FlowBuilder {
|
|
|
98
125
|
// Internal execution
|
|
99
126
|
// -----------------------------------------------------------------------
|
|
100
127
|
async _execute(shared, params, signal) {
|
|
128
|
+
const labels = /* @__PURE__ */ new Map();
|
|
129
|
+
for (let j = 0; j < this.steps.length; j++) {
|
|
130
|
+
const s = this.steps[j];
|
|
131
|
+
if (s.type === "label") labels.set(s.name, j);
|
|
132
|
+
}
|
|
101
133
|
for (let i = 0; i < this.steps.length; i++) {
|
|
102
134
|
signal?.throwIfAborted();
|
|
103
135
|
const step = this.steps[i];
|
|
136
|
+
if (step.type === "label") continue;
|
|
104
137
|
const meta = { index: i, type: step.type };
|
|
105
138
|
try {
|
|
106
139
|
for (const h of this._hooksList)
|
|
107
140
|
await h.beforeStep?.(meta, shared, params);
|
|
141
|
+
let gotoTarget;
|
|
108
142
|
const runBody = async () => {
|
|
109
143
|
switch (step.type) {
|
|
110
|
-
case "fn":
|
|
111
|
-
await this._retry(
|
|
144
|
+
case "fn": {
|
|
145
|
+
const result = await this._retry(
|
|
112
146
|
step.retries,
|
|
113
147
|
step.delaySec,
|
|
114
148
|
() => step.fn(shared, params)
|
|
115
149
|
);
|
|
150
|
+
if (typeof result === "string" && result.startsWith("\u2192"))
|
|
151
|
+
gotoTarget = result.slice(1);
|
|
116
152
|
break;
|
|
153
|
+
}
|
|
117
154
|
case "branch": {
|
|
118
155
|
const action = await this._retry(
|
|
119
156
|
step.retries,
|
|
@@ -122,12 +159,15 @@ var FlowBuilder = class _FlowBuilder {
|
|
|
122
159
|
);
|
|
123
160
|
const key = action ? String(action) : "default";
|
|
124
161
|
const fn = step.branches[key] ?? step.branches["default"];
|
|
125
|
-
if (fn)
|
|
126
|
-
await this._retry(
|
|
162
|
+
if (fn) {
|
|
163
|
+
const branchResult = await this._retry(
|
|
127
164
|
step.retries,
|
|
128
165
|
step.delaySec,
|
|
129
166
|
() => fn(shared, params)
|
|
130
167
|
);
|
|
168
|
+
if (typeof branchResult === "string" && branchResult.startsWith("\u2192"))
|
|
169
|
+
gotoTarget = branchResult.slice(1);
|
|
170
|
+
}
|
|
131
171
|
break;
|
|
132
172
|
}
|
|
133
173
|
case "loop":
|
|
@@ -151,17 +191,49 @@ var FlowBuilder = class _FlowBuilder {
|
|
|
151
191
|
else shared.__batchItem = prev;
|
|
152
192
|
break;
|
|
153
193
|
}
|
|
154
|
-
case "parallel":
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
194
|
+
case "parallel": {
|
|
195
|
+
const pfnWrappers = this._hooksList.map((h) => h.wrapParallelFn).filter((w) => w != null);
|
|
196
|
+
if (step.reducer) {
|
|
197
|
+
const drafts = [];
|
|
198
|
+
await Promise.all(
|
|
199
|
+
step.fns.map(async (fn, fi) => {
|
|
200
|
+
const draft = { ...shared };
|
|
201
|
+
drafts[fi] = draft;
|
|
202
|
+
const exec = () => this._retry(
|
|
203
|
+
step.retries,
|
|
204
|
+
step.delaySec,
|
|
205
|
+
() => fn(draft, params)
|
|
206
|
+
);
|
|
207
|
+
const wrapped2 = pfnWrappers.reduceRight(
|
|
208
|
+
(next, wrap) => () => wrap(meta, fi, next, draft, params),
|
|
209
|
+
async () => {
|
|
210
|
+
await exec();
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
await wrapped2();
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
step.reducer(shared, drafts);
|
|
217
|
+
} else {
|
|
218
|
+
await Promise.all(
|
|
219
|
+
step.fns.map((fn, fi) => {
|
|
220
|
+
const exec = () => this._retry(
|
|
221
|
+
step.retries,
|
|
222
|
+
step.delaySec,
|
|
223
|
+
() => fn(shared, params)
|
|
224
|
+
);
|
|
225
|
+
const wrapped2 = pfnWrappers.reduceRight(
|
|
226
|
+
(next, wrap) => () => wrap(meta, fi, next, shared, params),
|
|
227
|
+
async () => {
|
|
228
|
+
await exec();
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
return wrapped2();
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
}
|
|
164
235
|
break;
|
|
236
|
+
}
|
|
165
237
|
}
|
|
166
238
|
};
|
|
167
239
|
const { timeoutMs } = step;
|
|
@@ -174,11 +246,18 @@ var FlowBuilder = class _FlowBuilder {
|
|
|
174
246
|
await wrapped();
|
|
175
247
|
for (const h of this._hooksList)
|
|
176
248
|
await h.afterStep?.(meta, shared, params);
|
|
249
|
+
if (gotoTarget) {
|
|
250
|
+
const target = labels.get(gotoTarget);
|
|
251
|
+
if (target === void 0)
|
|
252
|
+
throw new Error(`goto target label "${gotoTarget}" not found`);
|
|
253
|
+
i = target;
|
|
254
|
+
}
|
|
177
255
|
} catch (err) {
|
|
256
|
+
if (err instanceof InterruptError) throw err;
|
|
178
257
|
for (const h of this._hooksList) h.onError?.(meta, err, shared, params);
|
|
179
258
|
if (err instanceof FlowError) throw err;
|
|
180
|
-
const
|
|
181
|
-
throw new FlowError(
|
|
259
|
+
const stepLabel = step.type === "fn" ? `step ${i}` : `${step.type} (step ${i})`;
|
|
260
|
+
throw new FlowError(stepLabel, err);
|
|
182
261
|
}
|
|
183
262
|
}
|
|
184
263
|
}
|
|
@@ -214,5 +293,6 @@ var FlowBuilder = class _FlowBuilder {
|
|
|
214
293
|
};
|
|
215
294
|
export {
|
|
216
295
|
FlowBuilder,
|
|
217
|
-
FlowError
|
|
296
|
+
FlowError,
|
|
297
|
+
InterruptError
|
|
218
298
|
};
|
|
@@ -22,4 +22,24 @@ declare module "../../Flowneer" {
|
|
|
22
22
|
}
|
|
23
23
|
declare const withMocks: FlowneerPlugin;
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
declare module "../../Flowneer" {
|
|
26
|
+
interface FlowBuilder<S, P> {
|
|
27
|
+
/** Throw if total step executions exceed `max` (default 1000). */
|
|
28
|
+
withStepLimit(max?: number): this;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
declare const withStepLimit: FlowneerPlugin;
|
|
32
|
+
|
|
33
|
+
declare module "../../Flowneer" {
|
|
34
|
+
interface FlowBuilder<S, P> {
|
|
35
|
+
/**
|
|
36
|
+
* Safe parallel execution with a reducer.
|
|
37
|
+
* Each fn receives its own shallow draft of `shared`.
|
|
38
|
+
* After all fns complete, `reducer(shared, drafts)` merges results.
|
|
39
|
+
*/
|
|
40
|
+
parallelAtomic(fns: NodeFn<S, P>[], reducer: (shared: S, drafts: S[]) => void, options?: NodeOptions): this;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
declare const withAtomicUpdates: FlowneerPlugin;
|
|
44
|
+
|
|
45
|
+
export { withAtomicUpdates, withDryRun, withMocks, withStepLimit };
|
|
@@ -25,7 +25,33 @@ var withMocks = {
|
|
|
25
25
|
return this;
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
|
|
29
|
+
// plugins/dev/withStepLimit.ts
|
|
30
|
+
var withStepLimit = {
|
|
31
|
+
withStepLimit(max = 1e3) {
|
|
32
|
+
let count = 0;
|
|
33
|
+
this._setHooks({
|
|
34
|
+
beforeFlow: () => {
|
|
35
|
+
count = 0;
|
|
36
|
+
},
|
|
37
|
+
beforeStep: () => {
|
|
38
|
+
if (++count > max)
|
|
39
|
+
throw new Error(`step limit exceeded: ${count} > ${max}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// plugins/dev/withAtomicUpdates.ts
|
|
47
|
+
var withAtomicUpdates = {
|
|
48
|
+
parallelAtomic(fns, reducer, options) {
|
|
49
|
+
return this.parallel(fns, options, reducer);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
28
52
|
export {
|
|
53
|
+
withAtomicUpdates,
|
|
29
54
|
withDryRun,
|
|
30
|
-
withMocks
|
|
55
|
+
withMocks,
|
|
56
|
+
withStepLimit
|
|
31
57
|
};
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { withHistory, withTiming, withVerbose } from './observability/index.js';
|
|
2
|
-
export { CircuitBreakerOptions, withCircuitBreaker, withFallback, withTimeout } from './resilience/index.js';
|
|
3
|
-
export { AuditEntry, AuditLogStore, CheckpointStore, withAuditLog, withCheckpoint, withReplay } from './persistence/index.js';
|
|
1
|
+
export { withHistory, withInterrupts, withTiming, withVerbose } from './observability/index.js';
|
|
2
|
+
export { CircuitBreakerOptions, withCircuitBreaker, withCycles, withFallback, withTimeout } from './resilience/index.js';
|
|
3
|
+
export { AuditEntry, AuditLogStore, CheckpointStore, VersionedCheckpointEntry, VersionedCheckpointStore, withAuditLog, withCheckpoint, withReplay, withVersionedCheckpoint } from './persistence/index.js';
|
|
4
4
|
export { RateLimitOptions, withCostTracker, withRateLimit, withTokenBudget } from './llm/index.js';
|
|
5
|
-
export { withDryRun, withMocks } from './dev/index.js';
|
|
5
|
+
export { withAtomicUpdates, withDryRun, withMocks, withStepLimit } from './dev/index.js';
|
|
6
|
+
export { StreamSubscriber, emit, peekChannel, receiveFrom, sendTo, withChannels, withStream } from './messaging/index.js';
|
|
6
7
|
import '../index.js';
|
package/dist/plugins/index.js
CHANGED
|
@@ -50,14 +50,43 @@ var withVerbose = {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
// Flowneer.ts
|
|
54
|
+
var InterruptError = class extends Error {
|
|
55
|
+
savedShared;
|
|
56
|
+
constructor(shared) {
|
|
57
|
+
super("Flow interrupted");
|
|
58
|
+
this.name = "InterruptError";
|
|
59
|
+
this.savedShared = shared;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// plugins/observability/withInterrupts.ts
|
|
64
|
+
var withInterrupts = {
|
|
65
|
+
interruptIf(condition) {
|
|
66
|
+
const interruptFn = async (shared, params) => {
|
|
67
|
+
const shouldInterrupt = await condition(shared, params);
|
|
68
|
+
if (shouldInterrupt) {
|
|
69
|
+
throw new InterruptError(JSON.parse(JSON.stringify(shared)));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return this.then(interruptFn);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
53
76
|
// plugins/resilience/withFallback.ts
|
|
54
77
|
var withFallback = {
|
|
55
78
|
withFallback(fn) {
|
|
56
79
|
this._setHooks({
|
|
57
|
-
wrapStep: async (
|
|
80
|
+
wrapStep: async (meta, next, shared, params) => {
|
|
58
81
|
try {
|
|
59
82
|
await next();
|
|
60
|
-
} catch {
|
|
83
|
+
} catch (e) {
|
|
84
|
+
shared.__fallbackError = {
|
|
85
|
+
stepIndex: meta.index,
|
|
86
|
+
stepType: meta.type,
|
|
87
|
+
message: e instanceof Error ? e.message : String(e),
|
|
88
|
+
stack: e instanceof Error ? e.stack : void 0
|
|
89
|
+
};
|
|
61
90
|
await fn(shared, params);
|
|
62
91
|
}
|
|
63
92
|
}
|
|
@@ -103,15 +132,44 @@ var withCircuitBreaker = {
|
|
|
103
132
|
var withTimeout = {
|
|
104
133
|
withTimeout(ms) {
|
|
105
134
|
this._setHooks({
|
|
106
|
-
wrapStep: (meta, next) =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
(
|
|
110
|
-
|
|
111
|
-
|
|
135
|
+
wrapStep: (meta, next) => {
|
|
136
|
+
let handle;
|
|
137
|
+
return Promise.race([
|
|
138
|
+
next().finally(() => clearTimeout(handle)),
|
|
139
|
+
new Promise(
|
|
140
|
+
(_, reject) => handle = setTimeout(
|
|
141
|
+
() => reject(
|
|
142
|
+
new Error(`step ${meta.index} timed out after ${ms}ms`)
|
|
143
|
+
),
|
|
144
|
+
ms
|
|
145
|
+
)
|
|
112
146
|
)
|
|
113
|
-
)
|
|
114
|
-
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// plugins/resilience/withCycles.ts
|
|
155
|
+
var withCycles = {
|
|
156
|
+
withCycles(maxJumps = 100) {
|
|
157
|
+
let jumps = 0;
|
|
158
|
+
this._setHooks({
|
|
159
|
+
beforeFlow: () => {
|
|
160
|
+
jumps = 0;
|
|
161
|
+
},
|
|
162
|
+
beforeStep: (meta) => {
|
|
163
|
+
if (meta.type === "fn" || meta.type === "branch") {
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
afterStep: (_meta, shared) => {
|
|
167
|
+
jumps++;
|
|
168
|
+
if (jumps > maxJumps)
|
|
169
|
+
throw new Error(
|
|
170
|
+
`cycle limit exceeded: ${jumps} step executions > maxJumps(${maxJumps})`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
115
173
|
});
|
|
116
174
|
return this;
|
|
117
175
|
}
|
|
@@ -169,6 +227,68 @@ var withReplay = {
|
|
|
169
227
|
}
|
|
170
228
|
};
|
|
171
229
|
|
|
230
|
+
// plugins/persistence/withVersionedCheckpoint.ts
|
|
231
|
+
function diffObjects(prev, curr) {
|
|
232
|
+
const diff = {};
|
|
233
|
+
for (const key of Object.keys(curr)) {
|
|
234
|
+
if (JSON.stringify(prev[key]) !== JSON.stringify(curr[key])) {
|
|
235
|
+
diff[key] = curr[key];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const key of Object.keys(prev)) {
|
|
239
|
+
if (!(key in curr)) {
|
|
240
|
+
diff[key] = void 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return diff;
|
|
244
|
+
}
|
|
245
|
+
var versionCounter = 0;
|
|
246
|
+
var withVersionedCheckpoint = {
|
|
247
|
+
withVersionedCheckpoint(store) {
|
|
248
|
+
let prevSnapshot = {};
|
|
249
|
+
let lastVersion = null;
|
|
250
|
+
this._setHooks({
|
|
251
|
+
beforeFlow: (shared) => {
|
|
252
|
+
prevSnapshot = JSON.parse(JSON.stringify(shared));
|
|
253
|
+
lastVersion = null;
|
|
254
|
+
},
|
|
255
|
+
afterStep: async (meta, shared) => {
|
|
256
|
+
const curr = JSON.parse(JSON.stringify(shared));
|
|
257
|
+
const diff = diffObjects(prevSnapshot, curr);
|
|
258
|
+
if (Object.keys(diff).length === 0) return;
|
|
259
|
+
const version = `v${++versionCounter}`;
|
|
260
|
+
const entry = {
|
|
261
|
+
version,
|
|
262
|
+
stepIndex: meta.index,
|
|
263
|
+
diff,
|
|
264
|
+
parentVersion: lastVersion,
|
|
265
|
+
timestamp: Date.now()
|
|
266
|
+
};
|
|
267
|
+
await store.save(entry);
|
|
268
|
+
lastVersion = version;
|
|
269
|
+
prevSnapshot = curr;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return this;
|
|
273
|
+
},
|
|
274
|
+
resumeFrom(version, store) {
|
|
275
|
+
let resolvedStep = null;
|
|
276
|
+
let resolved = false;
|
|
277
|
+
this._setHooks({
|
|
278
|
+
wrapStep: async (meta, next) => {
|
|
279
|
+
if (!resolved) {
|
|
280
|
+
const result = await store.resolve(version);
|
|
281
|
+
resolvedStep = result.stepIndex;
|
|
282
|
+
resolved = true;
|
|
283
|
+
}
|
|
284
|
+
if (resolvedStep !== null && meta.index <= resolvedStep) return;
|
|
285
|
+
await next();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
172
292
|
// plugins/llm/withTokenBudget.ts
|
|
173
293
|
var withTokenBudget = {
|
|
174
294
|
withTokenBudget(limit) {
|
|
@@ -247,19 +367,104 @@ var withMocks = {
|
|
|
247
367
|
return this;
|
|
248
368
|
}
|
|
249
369
|
};
|
|
370
|
+
|
|
371
|
+
// plugins/dev/withStepLimit.ts
|
|
372
|
+
var withStepLimit = {
|
|
373
|
+
withStepLimit(max = 1e3) {
|
|
374
|
+
let count = 0;
|
|
375
|
+
this._setHooks({
|
|
376
|
+
beforeFlow: () => {
|
|
377
|
+
count = 0;
|
|
378
|
+
},
|
|
379
|
+
beforeStep: () => {
|
|
380
|
+
if (++count > max)
|
|
381
|
+
throw new Error(`step limit exceeded: ${count} > ${max}`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// plugins/dev/withAtomicUpdates.ts
|
|
389
|
+
var withAtomicUpdates = {
|
|
390
|
+
parallelAtomic(fns, reducer, options) {
|
|
391
|
+
return this.parallel(fns, options, reducer);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// plugins/messaging/withChannels.ts
|
|
396
|
+
function sendTo(shared, channel, message) {
|
|
397
|
+
const channels = shared.__channels ?? /* @__PURE__ */ new Map();
|
|
398
|
+
if (!shared.__channels) shared.__channels = channels;
|
|
399
|
+
const queue = channels.get(channel) ?? [];
|
|
400
|
+
if (!channels.has(channel)) channels.set(channel, queue);
|
|
401
|
+
queue.push(message);
|
|
402
|
+
}
|
|
403
|
+
function receiveFrom(shared, channel) {
|
|
404
|
+
const channels = shared.__channels;
|
|
405
|
+
if (!channels) return [];
|
|
406
|
+
const queue = channels.get(channel);
|
|
407
|
+
if (!queue || queue.length === 0) return [];
|
|
408
|
+
const messages = [...queue];
|
|
409
|
+
queue.length = 0;
|
|
410
|
+
return messages;
|
|
411
|
+
}
|
|
412
|
+
function peekChannel(shared, channel) {
|
|
413
|
+
const channels = shared.__channels;
|
|
414
|
+
if (!channels) return [];
|
|
415
|
+
return [...channels.get(channel) ?? []];
|
|
416
|
+
}
|
|
417
|
+
var withChannels = {
|
|
418
|
+
withChannels() {
|
|
419
|
+
this._setHooks({
|
|
420
|
+
beforeFlow: (shared) => {
|
|
421
|
+
if (!shared.__channels) {
|
|
422
|
+
shared.__channels = /* @__PURE__ */ new Map();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// plugins/messaging/withStream.ts
|
|
431
|
+
var withStream = {
|
|
432
|
+
withStream(subscriber) {
|
|
433
|
+
this._setHooks({
|
|
434
|
+
beforeFlow: (shared) => {
|
|
435
|
+
shared.__stream = subscriber;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
return this;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
function emit(shared, chunk) {
|
|
442
|
+
shared.__stream?.(chunk);
|
|
443
|
+
}
|
|
250
444
|
export {
|
|
445
|
+
emit,
|
|
446
|
+
peekChannel,
|
|
447
|
+
receiveFrom,
|
|
448
|
+
sendTo,
|
|
449
|
+
withAtomicUpdates,
|
|
251
450
|
withAuditLog,
|
|
451
|
+
withChannels,
|
|
252
452
|
withCheckpoint,
|
|
253
453
|
withCircuitBreaker,
|
|
254
454
|
withCostTracker,
|
|
455
|
+
withCycles,
|
|
255
456
|
withDryRun,
|
|
256
457
|
withFallback,
|
|
257
458
|
withHistory,
|
|
459
|
+
withInterrupts,
|
|
258
460
|
withMocks,
|
|
259
461
|
withRateLimit,
|
|
260
462
|
withReplay,
|
|
463
|
+
withStepLimit,
|
|
464
|
+
withStream,
|
|
261
465
|
withTimeout,
|
|
262
466
|
withTiming,
|
|
263
467
|
withTokenBudget,
|
|
264
|
-
withVerbose
|
|
468
|
+
withVerbose,
|
|
469
|
+
withVersionedCheckpoint
|
|
265
470
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { FlowneerPlugin } from '../../index.js';
|
|
2
|
+
|
|
3
|
+
/** Send a message to a named channel on `shared.__channels`. */
|
|
4
|
+
declare function sendTo<S extends Record<string, any>>(shared: S, channel: string, message: unknown): void;
|
|
5
|
+
/** Receive (drain) all pending messages from a named channel. */
|
|
6
|
+
declare function receiveFrom<T = unknown>(shared: Record<string, any>, channel: string): T[];
|
|
7
|
+
/** Peek at pending messages without draining. */
|
|
8
|
+
declare function peekChannel<T = unknown>(shared: Record<string, any>, channel: string): T[];
|
|
9
|
+
declare module "../../Flowneer" {
|
|
10
|
+
interface FlowBuilder<S, P> {
|
|
11
|
+
/**
|
|
12
|
+
* Initialise a `Map`-based message channel system on `shared.__channels`.
|
|
13
|
+
* Nodes communicate via `sendTo(shared, ch, msg)` / `receiveFrom(shared, ch)`.
|
|
14
|
+
*/
|
|
15
|
+
withChannels(): this;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
declare const withChannels: FlowneerPlugin;
|
|
19
|
+
|
|
20
|
+
/** Callback invoked each time a step calls `emit()`. */
|
|
21
|
+
type StreamSubscriber<T = unknown> = (chunk: T) => void;
|
|
22
|
+
declare module "../../Flowneer" {
|
|
23
|
+
interface FlowBuilder<S, P> {
|
|
24
|
+
/**
|
|
25
|
+
* Registers a streaming subscriber for this flow.
|
|
26
|
+
* Steps call `emit(shared, chunk)` to push data to the subscriber in real-time.
|
|
27
|
+
* The subscriber is stored on `shared.__stream` before the first step runs
|
|
28
|
+
* so it is automatically inherited by sub-flows (loop, batch, etc.).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const flow = new FlowBuilder<MyState>()
|
|
32
|
+
* .withStream((chunk) => console.log("[stream]", chunk))
|
|
33
|
+
* // ...
|
|
34
|
+
*
|
|
35
|
+
* // Inside a step:
|
|
36
|
+
* emit(s, { type: "draft", content: s.draft });
|
|
37
|
+
*/
|
|
38
|
+
withStream<T = unknown>(subscriber: StreamSubscriber<T>): this;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
declare const withStream: FlowneerPlugin;
|
|
42
|
+
/**
|
|
43
|
+
* Push a chunk to the subscriber registered via `.withStream()`.
|
|
44
|
+
* Safe to call unconditionally — silently no-ops when no subscriber is registered.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* async function refineDraft(s: MyState) {
|
|
48
|
+
* // ... produce s.draft ...
|
|
49
|
+
* emit(s, { type: "draft", round: s.round, content: s.draft });
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
declare function emit<T = unknown>(shared: {
|
|
53
|
+
__stream?: StreamSubscriber<T>;
|
|
54
|
+
}, chunk: T): void;
|
|
55
|
+
|
|
56
|
+
export { type StreamSubscriber, emit, peekChannel, receiveFrom, sendTo, withChannels, withStream };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// plugins/messaging/withChannels.ts
|
|
2
|
+
function sendTo(shared, channel, message) {
|
|
3
|
+
const channels = shared.__channels ?? /* @__PURE__ */ new Map();
|
|
4
|
+
if (!shared.__channels) shared.__channels = channels;
|
|
5
|
+
const queue = channels.get(channel) ?? [];
|
|
6
|
+
if (!channels.has(channel)) channels.set(channel, queue);
|
|
7
|
+
queue.push(message);
|
|
8
|
+
}
|
|
9
|
+
function receiveFrom(shared, channel) {
|
|
10
|
+
const channels = shared.__channels;
|
|
11
|
+
if (!channels) return [];
|
|
12
|
+
const queue = channels.get(channel);
|
|
13
|
+
if (!queue || queue.length === 0) return [];
|
|
14
|
+
const messages = [...queue];
|
|
15
|
+
queue.length = 0;
|
|
16
|
+
return messages;
|
|
17
|
+
}
|
|
18
|
+
function peekChannel(shared, channel) {
|
|
19
|
+
const channels = shared.__channels;
|
|
20
|
+
if (!channels) return [];
|
|
21
|
+
return [...channels.get(channel) ?? []];
|
|
22
|
+
}
|
|
23
|
+
var withChannels = {
|
|
24
|
+
withChannels() {
|
|
25
|
+
this._setHooks({
|
|
26
|
+
beforeFlow: (shared) => {
|
|
27
|
+
if (!shared.__channels) {
|
|
28
|
+
shared.__channels = /* @__PURE__ */ new Map();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// plugins/messaging/withStream.ts
|
|
37
|
+
var withStream = {
|
|
38
|
+
withStream(subscriber) {
|
|
39
|
+
this._setHooks({
|
|
40
|
+
beforeFlow: (shared) => {
|
|
41
|
+
shared.__stream = subscriber;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function emit(shared, chunk) {
|
|
48
|
+
shared.__stream?.(chunk);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
emit,
|
|
52
|
+
peekChannel,
|
|
53
|
+
receiveFrom,
|
|
54
|
+
sendTo,
|
|
55
|
+
withChannels,
|
|
56
|
+
withStream
|
|
57
|
+
};
|
|
@@ -27,4 +27,19 @@ declare module "../../Flowneer" {
|
|
|
27
27
|
}
|
|
28
28
|
declare const withVerbose: FlowneerPlugin;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
declare module "../../Flowneer" {
|
|
31
|
+
interface FlowBuilder<S, P> {
|
|
32
|
+
/**
|
|
33
|
+
* Insert an interrupt point.
|
|
34
|
+
* If `condition(shared, params)` returns true, the flow throws
|
|
35
|
+
* an `InterruptError` carrying a deep clone of `shared`.
|
|
36
|
+
*
|
|
37
|
+
* Catch `InterruptError` in your runner to implement human-in-the-loop,
|
|
38
|
+
* approval gates, or external-resume patterns.
|
|
39
|
+
*/
|
|
40
|
+
interruptIf(condition: (shared: S, params: P) => boolean | Promise<boolean>): this;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
declare const withInterrupts: FlowneerPlugin;
|
|
44
|
+
|
|
45
|
+
export { withHistory, withInterrupts, withTiming, withVerbose };
|
|
@@ -49,8 +49,32 @@ var withVerbose = {
|
|
|
49
49
|
return this;
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
|
|
53
|
+
// Flowneer.ts
|
|
54
|
+
var InterruptError = class extends Error {
|
|
55
|
+
savedShared;
|
|
56
|
+
constructor(shared) {
|
|
57
|
+
super("Flow interrupted");
|
|
58
|
+
this.name = "InterruptError";
|
|
59
|
+
this.savedShared = shared;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// plugins/observability/withInterrupts.ts
|
|
64
|
+
var withInterrupts = {
|
|
65
|
+
interruptIf(condition) {
|
|
66
|
+
const interruptFn = async (shared, params) => {
|
|
67
|
+
const shouldInterrupt = await condition(shared, params);
|
|
68
|
+
if (shouldInterrupt) {
|
|
69
|
+
throw new InterruptError(JSON.parse(JSON.stringify(shared)));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return this.then(interruptFn);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
52
75
|
export {
|
|
53
76
|
withHistory,
|
|
77
|
+
withInterrupts,
|
|
54
78
|
withTiming,
|
|
55
79
|
withVerbose
|
|
56
80
|
};
|
|
@@ -48,4 +48,40 @@ declare module "../../Flowneer" {
|
|
|
48
48
|
}
|
|
49
49
|
declare const withReplay: FlowneerPlugin;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
interface VersionedCheckpointEntry<S = any> {
|
|
52
|
+
version: string;
|
|
53
|
+
stepIndex: number;
|
|
54
|
+
/** JSON-serialisable diff — only keys that changed from the previous version. */
|
|
55
|
+
diff: Partial<S>;
|
|
56
|
+
parentVersion: string | null;
|
|
57
|
+
timestamp: number;
|
|
58
|
+
}
|
|
59
|
+
interface VersionedCheckpointStore<S = any> {
|
|
60
|
+
/** Save a versioned checkpoint. Implementation assigns version ids. */
|
|
61
|
+
save(entry: VersionedCheckpointEntry<S>): void | Promise<void>;
|
|
62
|
+
/** Resolve a version id to the full snapshot + step index. */
|
|
63
|
+
resolve(version: string): {
|
|
64
|
+
stepIndex: number;
|
|
65
|
+
snapshot: S;
|
|
66
|
+
} | Promise<{
|
|
67
|
+
stepIndex: number;
|
|
68
|
+
snapshot: S;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
declare module "../../Flowneer" {
|
|
72
|
+
interface FlowBuilder<S, P> {
|
|
73
|
+
/**
|
|
74
|
+
* Save diff-based versioned checkpoints after each step.
|
|
75
|
+
* Each checkpoint records only the keys that changed, plus a parent pointer.
|
|
76
|
+
*/
|
|
77
|
+
withVersionedCheckpoint(store: VersionedCheckpointStore<S>): this;
|
|
78
|
+
/**
|
|
79
|
+
* Resume execution from a previously saved version.
|
|
80
|
+
* Steps before the saved `stepIndex` are skipped.
|
|
81
|
+
*/
|
|
82
|
+
resumeFrom(version: string, store: VersionedCheckpointStore<S>): this;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
declare const withVersionedCheckpoint: FlowneerPlugin;
|
|
86
|
+
|
|
87
|
+
export { type AuditEntry, type AuditLogStore, type CheckpointStore, type VersionedCheckpointEntry, type VersionedCheckpointStore, withAuditLog, withCheckpoint, withReplay, withVersionedCheckpoint };
|
|
@@ -49,8 +49,71 @@ var withReplay = {
|
|
|
49
49
|
return this;
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
|
|
53
|
+
// plugins/persistence/withVersionedCheckpoint.ts
|
|
54
|
+
function diffObjects(prev, curr) {
|
|
55
|
+
const diff = {};
|
|
56
|
+
for (const key of Object.keys(curr)) {
|
|
57
|
+
if (JSON.stringify(prev[key]) !== JSON.stringify(curr[key])) {
|
|
58
|
+
diff[key] = curr[key];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const key of Object.keys(prev)) {
|
|
62
|
+
if (!(key in curr)) {
|
|
63
|
+
diff[key] = void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return diff;
|
|
67
|
+
}
|
|
68
|
+
var versionCounter = 0;
|
|
69
|
+
var withVersionedCheckpoint = {
|
|
70
|
+
withVersionedCheckpoint(store) {
|
|
71
|
+
let prevSnapshot = {};
|
|
72
|
+
let lastVersion = null;
|
|
73
|
+
this._setHooks({
|
|
74
|
+
beforeFlow: (shared) => {
|
|
75
|
+
prevSnapshot = JSON.parse(JSON.stringify(shared));
|
|
76
|
+
lastVersion = null;
|
|
77
|
+
},
|
|
78
|
+
afterStep: async (meta, shared) => {
|
|
79
|
+
const curr = JSON.parse(JSON.stringify(shared));
|
|
80
|
+
const diff = diffObjects(prevSnapshot, curr);
|
|
81
|
+
if (Object.keys(diff).length === 0) return;
|
|
82
|
+
const version = `v${++versionCounter}`;
|
|
83
|
+
const entry = {
|
|
84
|
+
version,
|
|
85
|
+
stepIndex: meta.index,
|
|
86
|
+
diff,
|
|
87
|
+
parentVersion: lastVersion,
|
|
88
|
+
timestamp: Date.now()
|
|
89
|
+
};
|
|
90
|
+
await store.save(entry);
|
|
91
|
+
lastVersion = version;
|
|
92
|
+
prevSnapshot = curr;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return this;
|
|
96
|
+
},
|
|
97
|
+
resumeFrom(version, store) {
|
|
98
|
+
let resolvedStep = null;
|
|
99
|
+
let resolved = false;
|
|
100
|
+
this._setHooks({
|
|
101
|
+
wrapStep: async (meta, next) => {
|
|
102
|
+
if (!resolved) {
|
|
103
|
+
const result = await store.resolve(version);
|
|
104
|
+
resolvedStep = result.stepIndex;
|
|
105
|
+
resolved = true;
|
|
106
|
+
}
|
|
107
|
+
if (resolvedStep !== null && meta.index <= resolvedStep) return;
|
|
108
|
+
await next();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
52
114
|
export {
|
|
53
115
|
withAuditLog,
|
|
54
116
|
withCheckpoint,
|
|
55
|
-
withReplay
|
|
117
|
+
withReplay,
|
|
118
|
+
withVersionedCheckpoint
|
|
56
119
|
};
|
|
@@ -43,4 +43,16 @@ declare module "../../Flowneer" {
|
|
|
43
43
|
}
|
|
44
44
|
declare const withTimeout: FlowneerPlugin;
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
declare module "../../Flowneer" {
|
|
47
|
+
interface FlowBuilder<S, P> {
|
|
48
|
+
/**
|
|
49
|
+
* Guard against infinite goto loops.
|
|
50
|
+
* Tracks total label jumps per `run()` and throws if `maxJumps` is exceeded.
|
|
51
|
+
* Default: 100.
|
|
52
|
+
*/
|
|
53
|
+
withCycles(maxJumps?: number): this;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
declare const withCycles: FlowneerPlugin;
|
|
57
|
+
|
|
58
|
+
export { type CircuitBreakerOptions, withCircuitBreaker, withCycles, withFallback, withTimeout };
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
var withFallback = {
|
|
3
3
|
withFallback(fn) {
|
|
4
4
|
this._setHooks({
|
|
5
|
-
wrapStep: async (
|
|
5
|
+
wrapStep: async (meta, next, shared, params) => {
|
|
6
6
|
try {
|
|
7
7
|
await next();
|
|
8
|
-
} catch {
|
|
8
|
+
} catch (e) {
|
|
9
|
+
shared.__fallbackError = {
|
|
10
|
+
stepIndex: meta.index,
|
|
11
|
+
stepType: meta.type,
|
|
12
|
+
message: e instanceof Error ? e.message : String(e),
|
|
13
|
+
stack: e instanceof Error ? e.stack : void 0
|
|
14
|
+
};
|
|
9
15
|
await fn(shared, params);
|
|
10
16
|
}
|
|
11
17
|
}
|
|
@@ -51,21 +57,51 @@ var withCircuitBreaker = {
|
|
|
51
57
|
var withTimeout = {
|
|
52
58
|
withTimeout(ms) {
|
|
53
59
|
this._setHooks({
|
|
54
|
-
wrapStep: (meta, next) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
wrapStep: (meta, next) => {
|
|
61
|
+
let handle;
|
|
62
|
+
return Promise.race([
|
|
63
|
+
next().finally(() => clearTimeout(handle)),
|
|
64
|
+
new Promise(
|
|
65
|
+
(_, reject) => handle = setTimeout(
|
|
66
|
+
() => reject(
|
|
67
|
+
new Error(`step ${meta.index} timed out after ${ms}ms`)
|
|
68
|
+
),
|
|
69
|
+
ms
|
|
70
|
+
)
|
|
60
71
|
)
|
|
61
|
-
)
|
|
62
|
-
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// plugins/resilience/withCycles.ts
|
|
80
|
+
var withCycles = {
|
|
81
|
+
withCycles(maxJumps = 100) {
|
|
82
|
+
let jumps = 0;
|
|
83
|
+
this._setHooks({
|
|
84
|
+
beforeFlow: () => {
|
|
85
|
+
jumps = 0;
|
|
86
|
+
},
|
|
87
|
+
beforeStep: (meta) => {
|
|
88
|
+
if (meta.type === "fn" || meta.type === "branch") {
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
afterStep: (_meta, shared) => {
|
|
92
|
+
jumps++;
|
|
93
|
+
if (jumps > maxJumps)
|
|
94
|
+
throw new Error(
|
|
95
|
+
`cycle limit exceeded: ${jumps} step executions > maxJumps(${maxJumps})`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
63
98
|
});
|
|
64
99
|
return this;
|
|
65
100
|
}
|
|
66
101
|
};
|
|
67
102
|
export {
|
|
68
103
|
withCircuitBreaker,
|
|
104
|
+
withCycles,
|
|
69
105
|
withFallback,
|
|
70
106
|
withTimeout
|
|
71
107
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowneer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Zero-dependency fluent flow builder for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"./plugins/resilience": "./dist/plugins/resilience/index.js",
|
|
14
14
|
"./plugins/persistence": "./dist/plugins/persistence/index.js",
|
|
15
15
|
"./plugins/llm": "./dist/plugins/llm/index.js",
|
|
16
|
-
"./plugins/dev": "./dist/plugins/dev/index.js"
|
|
16
|
+
"./plugins/dev": "./dist/plugins/dev/index.js",
|
|
17
|
+
"./plugins/messaging": "./dist/plugins/messaging/index.js"
|
|
17
18
|
},
|
|
18
19
|
"sideEffects": false,
|
|
19
20
|
"files": [
|