flowneer 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -23
- package/dist/plugins/index.js +28 -11
- package/dist/plugins/resilience/index.d.ts +6 -3
- package/dist/plugins/resilience/index.js +28 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,59 @@ await new FlowBuilder<RefineState>()
|
|
|
259
259
|
|
|
260
260
|
> **Tip:** Pair with [`withCycles`](#withcycles) to cap the maximum number of jumps.
|
|
261
261
|
|
|
262
|
+
## using with `withCycles` plugin
|
|
263
|
+
|
|
264
|
+
`withCycles` guards against infinite anchor-jump loops. Each call registers one limit; multiple calls stack.
|
|
265
|
+
|
|
266
|
+
**Global limit** — throws after `n` total anchor jumps across the whole flow:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { FlowBuilder } from "flowneer";
|
|
270
|
+
import { withCycles } from "flowneer/plugins/resilience";
|
|
271
|
+
|
|
272
|
+
FlowBuilder.use(withCycles);
|
|
273
|
+
|
|
274
|
+
const flow = new FlowBuilder<State>()
|
|
275
|
+
.withCycles(5) // max 5 total anchor jumps
|
|
276
|
+
.startWith(async (s) => {
|
|
277
|
+
s.count = 0;
|
|
278
|
+
})
|
|
279
|
+
.anchor("loop")
|
|
280
|
+
.then(async (s) => {
|
|
281
|
+
s.count += 1;
|
|
282
|
+
if (s.count < 3) return "#loop"; // jump back to "loop" anchor
|
|
283
|
+
})
|
|
284
|
+
.then(async (s) => console.log("done, count =", s.count));
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Per-anchor limit** — pass an anchor name as the second argument to restrict visits to that specific anchor only:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const flow = new FlowBuilder<State>()
|
|
291
|
+
.withCycles(5, "refine") // max 5 visits to the "refine" anchor
|
|
292
|
+
.startWith(generateDraft)
|
|
293
|
+
.anchor("refine")
|
|
294
|
+
.then(async (s) => {
|
|
295
|
+
s.quality = await score(s.draft);
|
|
296
|
+
if (s.quality < 0.8) {
|
|
297
|
+
s.draft = await improve(s.draft);
|
|
298
|
+
return "#refine";
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Mixed** — combine a global cap with independent per-anchor limits by chaining calls. Each limit is evaluated independently:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const flow = new FlowBuilder<State>()
|
|
307
|
+
.withCycles(100) // global: max 100 total anchor jumps
|
|
308
|
+
.withCycles(5, "fast") // "fast" anchor: max 5 visits
|
|
309
|
+
.withCycles(10, "retry") // "retry" anchor: max 10 visits
|
|
310
|
+
...
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Unlisted anchors are unaffected by per-anchor limits. The global limit (if set) still counts every jump regardless of which anchor was targeted.
|
|
314
|
+
|
|
262
315
|
### `run(shared, params?, options?)`
|
|
263
316
|
|
|
264
317
|
Execute the flow. Optionally pass a `params` object that every step receives as a second argument.
|
|
@@ -340,29 +393,29 @@ A plugin is an object of functions that get copied onto `FlowBuilder.prototype`.
|
|
|
340
393
|
|
|
341
394
|
### Available plugins
|
|
342
395
|
|
|
343
|
-
| Category | Plugin | Method | Description
|
|
344
|
-
| ----------------- | ------------------------- | ----------------------------------------- |
|
|
345
|
-
| **Observability** | `withHistory` | `.withHistory()` | Appends a shallow state snapshot after each step to `shared.__history`
|
|
346
|
-
| | `withTiming` | `.withTiming()` | Records wall-clock duration (ms) of each step in `shared.__timings[index]`
|
|
347
|
-
| | `withVerbose` | `.withVerbose()` | Prints the full `shared` object to stdout after each step
|
|
348
|
-
| | `withInterrupts` | `.interruptIf(condition)` | Pauses the flow by throwing an `InterruptError` (with a deep-clone of `shared`) when condition is true
|
|
349
|
-
| **Persistence** | `withCheckpoint` | `.withCheckpoint(store)` | Saves `shared` to a store after each successful step
|
|
350
|
-
| | `withAuditLog` | `.withAuditLog(store)` | Writes an immutable deep-clone audit entry to a store after every step (success and error)
|
|
351
|
-
| | `withReplay` | `.withReplay(fromStep)` | Skips all steps before `fromStep`; combine with `.withCheckpoint()` to resume a failed flow
|
|
352
|
-
| | `withVersionedCheckpoint` | `.withVersionedCheckpoint(store)` | Saves diff-based versioned checkpoints with parent pointers after each step that changes state
|
|
353
|
-
| | | `.resumeFrom(version, store)` | Resolves a version id and skips all steps up to and including the saved step index
|
|
354
|
-
| **Resilience** | `withCircuitBreaker` | `.withCircuitBreaker(opts?)` | Opens the circuit after `maxFailures` consecutive failures and rejects all steps until `resetMs` elapses
|
|
355
|
-
| | `withFallback` | `.withFallback(fn)` | Catches any step error and calls `fn` instead of propagating, allowing the flow to continue
|
|
356
|
-
| | `withTimeout` | `.withTimeout(ms)` | Aborts any step that exceeds `ms` milliseconds with a descriptive error
|
|
357
|
-
| | `withCycles` | `.withCycles(
|
|
358
|
-
| **Messaging** | `withChannels` | `.withChannels()` | Initialises a `Map`-based message-channel system on `shared.__channels`
|
|
359
|
-
| **LLM** | `withCostTracker` | `.withCostTracker()` | Accumulates per-step `shared.__stepCost` values into `shared.__cost` after each step
|
|
360
|
-
| | `withRateLimit` | `.withRateLimit({ intervalMs })` | Enforces a minimum gap of `intervalMs` ms between steps to avoid hammering rate-limited APIs
|
|
361
|
-
| | `withTokenBudget` | `.withTokenBudget(limit)` | Aborts the flow before any step where `shared.tokensUsed >= limit`
|
|
362
|
-
| **Dev** | `withDryRun` | `.withDryRun()` | Skips all step bodies while still firing hooks — useful for validating observability wiring
|
|
363
|
-
| | `withMocks` | `.withMocks(map)` | Replaces step bodies at specified indices with mock functions; all other steps run normally
|
|
364
|
-
| | `withStepLimit` | `.withStepLimit(max?)` | Throws after `max` total step executions (default 1000); counter resets on each `run()` call
|
|
365
|
-
| | `withAtomicUpdates` | `.parallelAtomic(fns, reducer, options?)` | Sugar over `parallel()` with a reducer — each fn runs on an isolated draft, reducer merges results
|
|
396
|
+
| Category | Plugin | Method | Description |
|
|
397
|
+
| ----------------- | ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
398
|
+
| **Observability** | `withHistory` | `.withHistory()` | Appends a shallow state snapshot after each step to `shared.__history` |
|
|
399
|
+
| | `withTiming` | `.withTiming()` | Records wall-clock duration (ms) of each step in `shared.__timings[index]` |
|
|
400
|
+
| | `withVerbose` | `.withVerbose()` | Prints the full `shared` object to stdout after each step |
|
|
401
|
+
| | `withInterrupts` | `.interruptIf(condition)` | Pauses the flow by throwing an `InterruptError` (with a deep-clone of `shared`) when condition is true |
|
|
402
|
+
| **Persistence** | `withCheckpoint` | `.withCheckpoint(store)` | Saves `shared` to a store after each successful step |
|
|
403
|
+
| | `withAuditLog` | `.withAuditLog(store)` | Writes an immutable deep-clone audit entry to a store after every step (success and error) |
|
|
404
|
+
| | `withReplay` | `.withReplay(fromStep)` | Skips all steps before `fromStep`; combine with `.withCheckpoint()` to resume a failed flow |
|
|
405
|
+
| | `withVersionedCheckpoint` | `.withVersionedCheckpoint(store)` | Saves diff-based versioned checkpoints with parent pointers after each step that changes state |
|
|
406
|
+
| | | `.resumeFrom(version, store)` | Resolves a version id and skips all steps up to and including the saved step index |
|
|
407
|
+
| **Resilience** | `withCircuitBreaker` | `.withCircuitBreaker(opts?)` | Opens the circuit after `maxFailures` consecutive failures and rejects all steps until `resetMs` elapses |
|
|
408
|
+
| | `withFallback` | `.withFallback(fn)` | Catches any step error and calls `fn` instead of propagating, allowing the flow to continue |
|
|
409
|
+
| | `withTimeout` | `.withTimeout(ms)` | Aborts any step that exceeds `ms` milliseconds with a descriptive error |
|
|
410
|
+
| | `withCycles` | `.withCycles(n, anchor?)` | Throws after `n` anchor jumps globally, or after `n` visits to a named anchor — guards against infinite goto loops |
|
|
411
|
+
| **Messaging** | `withChannels` | `.withChannels()` | Initialises a `Map`-based message-channel system on `shared.__channels` |
|
|
412
|
+
| **LLM** | `withCostTracker` | `.withCostTracker()` | Accumulates per-step `shared.__stepCost` values into `shared.__cost` after each step |
|
|
413
|
+
| | `withRateLimit` | `.withRateLimit({ intervalMs })` | Enforces a minimum gap of `intervalMs` ms between steps to avoid hammering rate-limited APIs |
|
|
414
|
+
| | `withTokenBudget` | `.withTokenBudget(limit)` | Aborts the flow before any step where `shared.tokensUsed >= limit` |
|
|
415
|
+
| **Dev** | `withDryRun` | `.withDryRun()` | Skips all step bodies while still firing hooks — useful for validating observability wiring |
|
|
416
|
+
| | `withMocks` | `.withMocks(map)` | Replaces step bodies at specified indices with mock functions; all other steps run normally |
|
|
417
|
+
| | `withStepLimit` | `.withStepLimit(max?)` | Throws after `max` total step executions (default 1000); counter resets on each `run()` call |
|
|
418
|
+
| | `withAtomicUpdates` | `.parallelAtomic(fns, reducer, options?)` | Sugar over `parallel()` with a reducer — each fn runs on an isolated draft, reducer merges results |
|
|
366
419
|
|
|
367
420
|
Plugins are imported from `flowneer/plugins` (or their individual subpath) and registered once with `FlowBuilder.use()`:
|
|
368
421
|
|
package/dist/plugins/index.js
CHANGED
|
@@ -153,22 +153,39 @@ var withTimeout = {
|
|
|
153
153
|
|
|
154
154
|
// plugins/resilience/withCycles.ts
|
|
155
155
|
var withCycles = {
|
|
156
|
-
withCycles(maxJumps
|
|
157
|
-
let
|
|
156
|
+
withCycles(maxJumps, anchor) {
|
|
157
|
+
let count = 0;
|
|
158
|
+
let prevIndex = -1;
|
|
158
159
|
this._setHooks({
|
|
159
160
|
beforeFlow: () => {
|
|
160
|
-
|
|
161
|
+
count = 0;
|
|
162
|
+
prevIndex = -1;
|
|
161
163
|
},
|
|
162
164
|
beforeStep: (meta) => {
|
|
163
|
-
if (
|
|
165
|
+
if (prevIndex !== -1 && meta.index <= prevIndex) {
|
|
166
|
+
let jumpedAnchor;
|
|
167
|
+
for (let k = meta.index - 1; k >= 0; k--) {
|
|
168
|
+
const s = this.steps[k];
|
|
169
|
+
if (s?.type === "anchor") {
|
|
170
|
+
jumpedAnchor = s.name;
|
|
171
|
+
} else {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
count++;
|
|
176
|
+
if (anchor === void 0) {
|
|
177
|
+
if (count > maxJumps)
|
|
178
|
+
throw new Error(
|
|
179
|
+
`cycle limit exceeded: ${count} anchor jumps > maxJumps(${maxJumps})`
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
if (jumpedAnchor === anchor && count > maxJumps)
|
|
183
|
+
throw new Error(
|
|
184
|
+
`cycle limit exceeded for anchor "${anchor}": ${count} visits > limit(${maxJumps})`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
164
187
|
}
|
|
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
|
-
);
|
|
188
|
+
prevIndex = meta.index;
|
|
172
189
|
}
|
|
173
190
|
});
|
|
174
191
|
return this;
|
|
@@ -47,10 +47,13 @@ declare module "../../Flowneer" {
|
|
|
47
47
|
interface FlowBuilder<S, P> {
|
|
48
48
|
/**
|
|
49
49
|
* Guard against infinite goto loops.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* - `withCycles(n)` — throws after `n` total anchor jumps per `run()`.
|
|
52
|
+
* - `withCycles(n, "anchorName")` — throws after `n` visits to the named
|
|
53
|
+
* anchor via goto. The global limit (if also set) still applies.
|
|
54
|
+
* - Both forms can be combined: `.withCycles(100).withCycles(5, "fast")`
|
|
52
55
|
*/
|
|
53
|
-
withCycles(maxJumps?:
|
|
56
|
+
withCycles(maxJumps: number, anchor?: string): this;
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
declare const withCycles: FlowneerPlugin;
|
|
@@ -78,22 +78,39 @@ var withTimeout = {
|
|
|
78
78
|
|
|
79
79
|
// plugins/resilience/withCycles.ts
|
|
80
80
|
var withCycles = {
|
|
81
|
-
withCycles(maxJumps
|
|
82
|
-
let
|
|
81
|
+
withCycles(maxJumps, anchor) {
|
|
82
|
+
let count = 0;
|
|
83
|
+
let prevIndex = -1;
|
|
83
84
|
this._setHooks({
|
|
84
85
|
beforeFlow: () => {
|
|
85
|
-
|
|
86
|
+
count = 0;
|
|
87
|
+
prevIndex = -1;
|
|
86
88
|
},
|
|
87
89
|
beforeStep: (meta) => {
|
|
88
|
-
if (
|
|
90
|
+
if (prevIndex !== -1 && meta.index <= prevIndex) {
|
|
91
|
+
let jumpedAnchor;
|
|
92
|
+
for (let k = meta.index - 1; k >= 0; k--) {
|
|
93
|
+
const s = this.steps[k];
|
|
94
|
+
if (s?.type === "anchor") {
|
|
95
|
+
jumpedAnchor = s.name;
|
|
96
|
+
} else {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
count++;
|
|
101
|
+
if (anchor === void 0) {
|
|
102
|
+
if (count > maxJumps)
|
|
103
|
+
throw new Error(
|
|
104
|
+
`cycle limit exceeded: ${count} anchor jumps > maxJumps(${maxJumps})`
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
if (jumpedAnchor === anchor && count > maxJumps)
|
|
108
|
+
throw new Error(
|
|
109
|
+
`cycle limit exceeded for anchor "${anchor}": ${count} visits > limit(${maxJumps})`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
89
112
|
}
|
|
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
|
-
);
|
|
113
|
+
prevIndex = meta.index;
|
|
97
114
|
}
|
|
98
115
|
});
|
|
99
116
|
return this;
|