@fkws/klonk 0.0.12 → 0.0.14
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 +18 -7
- package/dist/index.cjs +63 -73
- package/dist/index.d.ts +30 -18
- package/dist/index.js +63 -60
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -64,11 +64,21 @@ A `Machine` is a finite state machine. It's made up of `StateNode`s. Each `State
|
|
|
64
64
|
|
|
65
65
|
The `Machine` carries a mutable `stateData` object that can be read from and written to by playlists and transition conditions throughout its execution.
|
|
66
66
|
|
|
67
|
+
#### Machine run modes
|
|
68
|
+
- **any**: Runs until the first terminal condition occurs (leaf state, roundtrip to the initial state, or all reachable states visited).
|
|
69
|
+
- **leaf**: Runs until a leaf state (no transitions) is reached.
|
|
70
|
+
- **roundtrip**: Runs until it transitions back to the initial state.
|
|
71
|
+
- **infinitely**: Continues running indefinitely, sleeping between iterations (`interval` ms, default 1000). Use `stopAfter` to cap total states entered.
|
|
72
|
+
|
|
73
|
+
Notes:
|
|
74
|
+
- `stopAfter` counts states entered, including the initial state. For example, `stopAfter: 1` will run the initial state's playlist once and then stop; `stopAfter: 0` stops before entering the initial state.
|
|
75
|
+
- Retries are independent of `stopAfter`. A state can retry its transition condition (with optional delay) without affecting the `stopAfter` count until a state transition actually occurs.
|
|
76
|
+
|
|
67
77
|
## Features
|
|
68
78
|
- **Type-Safe & Autocompleted**: Klonk leverages TypeScript's inference to provide a world-class developer experience. The inputs and outputs of every step are strongly typed, so you'll know at compile time if your logic is sound.
|
|
69
79
|
- **Code-First**: Define your automations directly in TypeScript. No YAML, no drag-and-drop UIs. Just the full power of a real programming language.
|
|
70
80
|
- **Composable & Extensible**: The core primitives (`Task`, `Trigger`) are simple abstract classes, making it easy to create your own reusable components and integrations.
|
|
71
|
-
- **Flexible Execution**: `Machines`
|
|
81
|
+
- **Flexible Execution**: `Machines` run with configurable modes via `run(state, options)`: `any`, `leaf`, `roundtrip`, or `infinitely` (with optional `interval`).
|
|
72
82
|
|
|
73
83
|
## Klonkworks: Pre-built Components
|
|
74
84
|
Coming soon(ish)! Klonkworks will be a large collection of pre-built Tasks, Triggers, and integrations. This will allow you to quickly assemble powerful automations that connect to a wide variety of services, often without needing to build your own components from scratch.
|
|
@@ -445,8 +455,9 @@ const webSearchAgent = Machine
|
|
|
445
455
|
}
|
|
446
456
|
})
|
|
447
457
|
))
|
|
448
|
-
.
|
|
449
|
-
|
|
458
|
+
.addLogger(pino()) // If you add a logger to your machine,
|
|
459
|
+
// it will call its info(), error(), debug(), fatal(), warn(), and trace() methods. Pino is recommended.
|
|
460
|
+
.finalize({ // Finalize your machine to make it ready to run. If you don't provide an ident, a uuidv4 will be generated for it.
|
|
450
461
|
ident: "web-search-agent"
|
|
451
462
|
})
|
|
452
463
|
|
|
@@ -457,10 +468,10 @@ const state: StateData = { // The state object is mutable and is passed to the m
|
|
|
457
468
|
model: "openai/gpt-4o-mini"
|
|
458
469
|
}
|
|
459
470
|
|
|
460
|
-
// The .run() method executes the machine until it reaches a terminal
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
const finalState = await webSearchAgent.run(state)
|
|
471
|
+
// The .run() method executes the machine until it reaches a terminal condition
|
|
472
|
+
// based on the selected mode. For example, 'roundtrip' stops when it returns
|
|
473
|
+
// to the initial state. The original state object is also mutated.
|
|
474
|
+
const finalState = await webSearchAgent.run(state, { mode: 'roundtrip' })
|
|
464
475
|
|
|
465
476
|
console.log(finalState.finalResponse) // The final state is returned.
|
|
466
477
|
// Or simply:
|
package/dist/index.cjs
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
var import_node_module = require("node:module");
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
var __defProp = Object.defineProperty;
|
|
5
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
-
for (let key of __getOwnPropNames(mod))
|
|
12
|
-
if (!__hasOwnProp.call(to, key))
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
6
|
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
20
7
|
var __toCommonJS = (from) => {
|
|
21
8
|
var entry = __moduleCache.get(from), desc;
|
|
@@ -181,22 +168,18 @@ class Trigger {
|
|
|
181
168
|
}
|
|
182
169
|
}
|
|
183
170
|
// src/prototypes/Machine.ts
|
|
184
|
-
var import_pino = __toESM(require("pino"));
|
|
185
171
|
var import_crypto = require("crypto");
|
|
186
|
-
var glogger = null;
|
|
187
|
-
|
|
188
172
|
class StateNode {
|
|
189
173
|
transitions;
|
|
190
174
|
playlist;
|
|
191
|
-
timeToNextTick;
|
|
192
175
|
ident;
|
|
193
176
|
tempTransitions;
|
|
194
177
|
retry;
|
|
195
178
|
maxRetries;
|
|
179
|
+
logger;
|
|
196
180
|
constructor(transitions, playlist) {
|
|
197
181
|
this.transitions = transitions;
|
|
198
182
|
this.playlist = playlist;
|
|
199
|
-
this.timeToNextTick = 1000;
|
|
200
183
|
this.ident = "";
|
|
201
184
|
this.retry = 1000;
|
|
202
185
|
this.maxRetries = false;
|
|
@@ -255,7 +238,7 @@ class StateNode {
|
|
|
255
238
|
return null;
|
|
256
239
|
}
|
|
257
240
|
async next(data) {
|
|
258
|
-
const logger =
|
|
241
|
+
const logger = this.logger?.child?.({ path: "StateNode.next", state: this.ident }) ?? this.logger;
|
|
259
242
|
logger?.info({ phase: "start" }, "Evaluating next state");
|
|
260
243
|
const sorted = [...this.transitions || []].map((t, i) => ({ t, i })).sort((a, b) => (b.t.weight ?? 0) - (a.t.weight ?? 0) || a.i - b.i);
|
|
261
244
|
for (const { t } of sorted) {
|
|
@@ -280,8 +263,10 @@ class Machine {
|
|
|
280
263
|
finalized = false;
|
|
281
264
|
logger;
|
|
282
265
|
ident;
|
|
266
|
+
started = false;
|
|
267
|
+
tickTimer = null;
|
|
283
268
|
getAllStates() {
|
|
284
|
-
const logger =
|
|
269
|
+
const logger = this.logger?.child?.({ path: "machine.getAllStates" }) ?? this.logger;
|
|
285
270
|
logger?.info({ phase: "start" }, "Gathering all states...");
|
|
286
271
|
if (!this.initialState)
|
|
287
272
|
return [];
|
|
@@ -310,10 +295,9 @@ class Machine {
|
|
|
310
295
|
return new Machine;
|
|
311
296
|
}
|
|
312
297
|
finalize({
|
|
313
|
-
verbose,
|
|
314
298
|
ident
|
|
315
299
|
} = {}) {
|
|
316
|
-
const logger =
|
|
300
|
+
const logger = this.logger?.child?.({ path: "machine.finalize", instance: this.ident }) ?? this.logger;
|
|
317
301
|
if (!this.initialState || this.statesToCreate.length === 0) {
|
|
318
302
|
logger?.error({ phase: "error" }, "Finalization failed: no initial state or states to create");
|
|
319
303
|
throw new Error("Cannot finalize a machine without an initial state or states to create.");
|
|
@@ -323,13 +307,11 @@ class Machine {
|
|
|
323
307
|
} else {
|
|
324
308
|
this.ident = import_crypto.randomUUID();
|
|
325
309
|
}
|
|
326
|
-
if (verbose)
|
|
327
|
-
glogger = import_pino.default();
|
|
328
|
-
logger?.info("Logging enabled.");
|
|
329
310
|
logger?.info({ phase: "start" }, `Finalizing machine ${this.ident}...`);
|
|
330
311
|
const registry = new Map;
|
|
331
312
|
logger?.info({ phase: "progress" }, `Building state registry...`);
|
|
332
313
|
for (const s of this.statesToCreate) {
|
|
314
|
+
s.logger = this.logger;
|
|
333
315
|
if (!s.ident) {
|
|
334
316
|
logger?.error({ phase: "error" }, "Finalization failed: state missing ident");
|
|
335
317
|
throw new Error("State missing ident.");
|
|
@@ -361,7 +343,7 @@ class Machine {
|
|
|
361
343
|
return this;
|
|
362
344
|
}
|
|
363
345
|
addState(state, options = {}) {
|
|
364
|
-
const logger =
|
|
346
|
+
const logger = this.logger?.child?.({ path: "machine.addState", instance: this.ident }) ?? this.logger;
|
|
365
347
|
logger?.info({ phase: "start", state: state.ident, isInitial: !!options.initial }, "Adding state");
|
|
366
348
|
this.statesToCreate.push(state);
|
|
367
349
|
if (options.initial) {
|
|
@@ -370,45 +352,29 @@ class Machine {
|
|
|
370
352
|
logger?.info({ phase: "end", state: state.ident }, "State added");
|
|
371
353
|
return this;
|
|
372
354
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
await this.currentState.playlist.run(stateData);
|
|
390
|
-
logger?.info({ phase: "progress", state: this.currentState.ident }, "Initial playlist run complete.");
|
|
391
|
-
}
|
|
392
|
-
const tick = async () => {
|
|
393
|
-
const tickLogger = logger?.child({ path: "machine.tick" });
|
|
394
|
-
tickLogger?.info({ phase: "start", state: this.currentState.ident }, "Tick.");
|
|
395
|
-
const next = await this.currentState.next(stateData);
|
|
396
|
-
if (next) {
|
|
397
|
-
tickLogger?.info({ phase: "progress", from: this.currentState.ident, to: next.ident }, "Transitioning state.");
|
|
398
|
-
this.currentState = next;
|
|
399
|
-
await this.currentState.playlist.run(stateData);
|
|
400
|
-
tickLogger?.info({ phase: "progress", state: this.currentState.ident }, "Playlist run complete.");
|
|
401
|
-
} else {
|
|
402
|
-
tickLogger?.info({ phase: "progress", state: this.currentState.ident }, "No next state.");
|
|
355
|
+
addLogger(logger) {
|
|
356
|
+
this.logger = logger;
|
|
357
|
+
if (this.initialState) {
|
|
358
|
+
const visited = new Set;
|
|
359
|
+
const stack = [this.initialState];
|
|
360
|
+
while (stack.length > 0) {
|
|
361
|
+
const node = stack.pop();
|
|
362
|
+
if (!node.ident || visited.has(node.ident))
|
|
363
|
+
continue;
|
|
364
|
+
visited.add(node.ident);
|
|
365
|
+
node.logger = logger;
|
|
366
|
+
for (const tr of node.transitions || []) {
|
|
367
|
+
if (tr.to && tr.to.ident && !visited.has(tr.to.ident)) {
|
|
368
|
+
stack.push(tr.to);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
403
371
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
tick();
|
|
407
|
-
logger?.info({ phase: "end" }, "Machine started, tick loop initiated.");
|
|
372
|
+
}
|
|
373
|
+
return this;
|
|
408
374
|
}
|
|
409
|
-
async run(stateData) {
|
|
410
|
-
const logger =
|
|
411
|
-
logger?.info({ phase: "start" }, "Running machine...");
|
|
375
|
+
async run(stateData, options) {
|
|
376
|
+
const logger = this.logger?.child?.({ path: "machine.run", instance: this.ident }) ?? this.logger;
|
|
377
|
+
logger?.info({ phase: "start", options }, "Running machine...");
|
|
412
378
|
if (!this.finalized) {
|
|
413
379
|
logger?.error({ phase: "error" }, "Machine not finalized");
|
|
414
380
|
throw new Error("Cannot run a machine that is not finalized.");
|
|
@@ -419,27 +385,39 @@ class Machine {
|
|
|
419
385
|
}
|
|
420
386
|
const allStates = this.getAllStates();
|
|
421
387
|
const visitedIdents = new Set;
|
|
388
|
+
let transitionsCount = 0;
|
|
389
|
+
if (options.stopAfter !== undefined && options.stopAfter === 0) {
|
|
390
|
+
logger?.info({ phase: "end", reason: "stopAfter", count: transitionsCount }, "Stop condition met.");
|
|
391
|
+
return stateData;
|
|
392
|
+
}
|
|
422
393
|
let current = this.initialState;
|
|
423
394
|
logger?.info({ phase: "progress", state: current.ident }, "Set initial state. Running playlist.");
|
|
424
395
|
await current.playlist.run(stateData);
|
|
396
|
+
transitionsCount = 1;
|
|
425
397
|
visitedIdents.add(current.ident);
|
|
398
|
+
if (options.stopAfter !== undefined && transitionsCount >= options.stopAfter) {
|
|
399
|
+
logger?.info({ phase: "end", reason: "stopAfter", count: transitionsCount }, "Stop condition met.");
|
|
400
|
+
return stateData;
|
|
401
|
+
}
|
|
426
402
|
while (true) {
|
|
427
403
|
if (!current.transitions || current.transitions.length === 0) {
|
|
428
|
-
|
|
429
|
-
|
|
404
|
+
if (options.mode !== "infinitely") {
|
|
405
|
+
logger?.info({ phase: "end", state: current.ident, reason: "leaf" }, "Stop condition met.");
|
|
406
|
+
return stateData;
|
|
407
|
+
}
|
|
430
408
|
}
|
|
431
409
|
let next = await current.next(stateData);
|
|
432
410
|
if (!next) {
|
|
433
411
|
const retryDelay = current.retry;
|
|
434
|
-
if (
|
|
435
|
-
logger?.info({ phase: "end", state: current.ident }, "
|
|
412
|
+
if (retryDelay === false) {
|
|
413
|
+
logger?.info({ phase: "end", state: current.ident, reason: "no-transition-no-retry" }, "Stop condition met.");
|
|
436
414
|
return stateData;
|
|
437
415
|
}
|
|
438
416
|
let retries = 0;
|
|
439
417
|
logger?.info({ phase: "progress", state: current.ident, retryDelay }, "No next state, beginning retry logic.");
|
|
440
418
|
while (!next) {
|
|
441
|
-
if (current.maxRetries && retries >= current.maxRetries) {
|
|
442
|
-
logger?.warn({ phase: "end", state: current.ident, retries }, "
|
|
419
|
+
if (current.maxRetries !== false && retries >= current.maxRetries) {
|
|
420
|
+
logger?.warn({ phase: "end", state: current.ident, retries, reason: "retries-exhausted" }, "Stop condition met.");
|
|
443
421
|
return stateData;
|
|
444
422
|
}
|
|
445
423
|
await this.sleep(retryDelay);
|
|
@@ -454,17 +432,29 @@ class Machine {
|
|
|
454
432
|
}
|
|
455
433
|
const resolvedNext = next;
|
|
456
434
|
if (resolvedNext === this.initialState) {
|
|
457
|
-
|
|
458
|
-
|
|
435
|
+
if (options.mode !== "infinitely") {
|
|
436
|
+
logger?.info({ phase: "end", reason: "roundtrip" }, "Stop condition met.");
|
|
437
|
+
return stateData;
|
|
438
|
+
}
|
|
459
439
|
}
|
|
460
440
|
logger?.info({ phase: "progress", from: current.ident, to: resolvedNext.ident }, "Transitioning state.");
|
|
461
441
|
current = resolvedNext;
|
|
462
442
|
await current.playlist.run(stateData);
|
|
463
443
|
visitedIdents.add(current.ident);
|
|
464
|
-
|
|
465
|
-
|
|
444
|
+
transitionsCount++;
|
|
445
|
+
if (options.stopAfter !== undefined && transitionsCount >= options.stopAfter) {
|
|
446
|
+
logger?.info({ phase: "end", reason: "stopAfter", count: transitionsCount }, "Stop condition met.");
|
|
466
447
|
return stateData;
|
|
467
448
|
}
|
|
449
|
+
if (visitedIdents.size >= allStates.length) {
|
|
450
|
+
if (options.mode === "any") {
|
|
451
|
+
logger?.info({ phase: "end", reason: "all-visited" }, "Stop condition met.");
|
|
452
|
+
return stateData;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (options.mode === "infinitely") {
|
|
456
|
+
await this.sleep(options.interval ?? 1000);
|
|
457
|
+
}
|
|
468
458
|
}
|
|
469
459
|
}
|
|
470
460
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -282,7 +282,15 @@ declare class Workflow<
|
|
|
282
282
|
*/
|
|
283
283
|
static create(): Workflow<never, {}, null>;
|
|
284
284
|
}
|
|
285
|
-
|
|
285
|
+
type Logger = {
|
|
286
|
+
info: (...args: any[]) => void
|
|
287
|
+
error: (...args: any[]) => void
|
|
288
|
+
debug: (...args: any[]) => void
|
|
289
|
+
fatal: (...args: any[]) => void
|
|
290
|
+
warn: (...args: any[]) => void
|
|
291
|
+
trace: (...args: any[]) => void
|
|
292
|
+
child?: (bindings: Record<string, unknown>) => Logger
|
|
293
|
+
};
|
|
286
294
|
/**
|
|
287
295
|
* A weighted, conditional edge to a target state.
|
|
288
296
|
* Transitions are evaluated in descending `weight` order, then by insertion order.
|
|
@@ -307,7 +315,6 @@ type Transition<TStateData> = {
|
|
|
307
315
|
declare class StateNode<TStateData> {
|
|
308
316
|
transitions?: Transition<TStateData>[];
|
|
309
317
|
playlist: Playlist<any, TStateData>;
|
|
310
|
-
timeToNextTick: number;
|
|
311
318
|
ident: string;
|
|
312
319
|
tempTransitions?: {
|
|
313
320
|
to: string
|
|
@@ -316,6 +323,7 @@ declare class StateNode<TStateData> {
|
|
|
316
323
|
}[];
|
|
317
324
|
retry: false | number;
|
|
318
325
|
maxRetries: false | number;
|
|
326
|
+
logger?: Logger;
|
|
319
327
|
/**
|
|
320
328
|
* Create a `StateNode`.
|
|
321
329
|
*
|
|
@@ -417,6 +425,8 @@ declare class Machine2<TStateData> {
|
|
|
417
425
|
finalized: boolean;
|
|
418
426
|
logger?: Logger;
|
|
419
427
|
ident?: string;
|
|
428
|
+
private started;
|
|
429
|
+
private tickTimer;
|
|
420
430
|
/**
|
|
421
431
|
* Compute the set of reachable states starting from the initial state.
|
|
422
432
|
* Used by `run` to determine completion once all states have been visited.
|
|
@@ -448,8 +458,7 @@ declare class Machine2<TStateData> {
|
|
|
448
458
|
* @returns This machine for chaining.
|
|
449
459
|
* @throws If there is no initial state, no states have been added, a state is missing an ident, or idents are duplicated.
|
|
450
460
|
*/
|
|
451
|
-
finalize({
|
|
452
|
-
verbose?: boolean
|
|
461
|
+
finalize({ ident }?: {
|
|
453
462
|
ident?: string
|
|
454
463
|
}): Machine2<TStateData>;
|
|
455
464
|
/**
|
|
@@ -464,19 +473,10 @@ declare class Machine2<TStateData> {
|
|
|
464
473
|
initial?: boolean
|
|
465
474
|
}): Machine2<TStateData>;
|
|
466
475
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* The method returns immediately after initializing the loop.
|
|
470
|
-
*
|
|
471
|
-
* @param stateData - Mutable external state provided to playlists and transition conditions.
|
|
472
|
-
* @param options - Start options.
|
|
473
|
-
* @param options.interval - Tick interval in milliseconds (default 1000ms).
|
|
474
|
-
* @returns A promise that resolves once the loop has been initiated.
|
|
475
|
-
* @throws If the machine has not been finalized or no initial state is set.
|
|
476
|
+
* Attach a logger to this machine. If the machine has an initial state set,
|
|
477
|
+
* the logger will be propagated to all currently reachable states.
|
|
476
478
|
*/
|
|
477
|
-
|
|
478
|
-
interval?: number
|
|
479
|
-
}): Promise<void>;
|
|
479
|
+
addLogger(logger: Logger): Machine2<TStateData>;
|
|
480
480
|
/**
|
|
481
481
|
* Run the machine synchronously until it reaches a terminal condition:
|
|
482
482
|
* - A leaf state (no transitions)
|
|
@@ -489,6 +489,18 @@ declare class Machine2<TStateData> {
|
|
|
489
489
|
* @returns A promise that resolves once the run completes.
|
|
490
490
|
* @throws If the machine has not been finalized or no initial state is set.
|
|
491
491
|
*/
|
|
492
|
-
run(stateData: TStateData): Promise<TStateData>;
|
|
492
|
+
run(stateData: TStateData, options: RunOptions): Promise<TStateData>;
|
|
493
493
|
}
|
|
494
|
-
|
|
494
|
+
type RunOptions = {
|
|
495
|
+
stopAfter?: number
|
|
496
|
+
} & ({
|
|
497
|
+
mode: "any"
|
|
498
|
+
} | {
|
|
499
|
+
mode: "leaf"
|
|
500
|
+
} | {
|
|
501
|
+
mode: "roundtrip"
|
|
502
|
+
} | {
|
|
503
|
+
mode: "infinitely"
|
|
504
|
+
interval?: number
|
|
505
|
+
});
|
|
506
|
+
export { Workflow, TriggerEvent, Trigger, Task, StateNode, RunOptions, Railroad, Playlist, Machine2 as Machine };
|
package/dist/index.js
CHANGED
|
@@ -127,22 +127,18 @@ class Trigger {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
// src/prototypes/Machine.ts
|
|
130
|
-
import pino from "pino";
|
|
131
130
|
import { randomUUID } from "crypto";
|
|
132
|
-
var glogger = null;
|
|
133
|
-
|
|
134
131
|
class StateNode {
|
|
135
132
|
transitions;
|
|
136
133
|
playlist;
|
|
137
|
-
timeToNextTick;
|
|
138
134
|
ident;
|
|
139
135
|
tempTransitions;
|
|
140
136
|
retry;
|
|
141
137
|
maxRetries;
|
|
138
|
+
logger;
|
|
142
139
|
constructor(transitions, playlist) {
|
|
143
140
|
this.transitions = transitions;
|
|
144
141
|
this.playlist = playlist;
|
|
145
|
-
this.timeToNextTick = 1000;
|
|
146
142
|
this.ident = "";
|
|
147
143
|
this.retry = 1000;
|
|
148
144
|
this.maxRetries = false;
|
|
@@ -201,7 +197,7 @@ class StateNode {
|
|
|
201
197
|
return null;
|
|
202
198
|
}
|
|
203
199
|
async next(data) {
|
|
204
|
-
const logger =
|
|
200
|
+
const logger = this.logger?.child?.({ path: "StateNode.next", state: this.ident }) ?? this.logger;
|
|
205
201
|
logger?.info({ phase: "start" }, "Evaluating next state");
|
|
206
202
|
const sorted = [...this.transitions || []].map((t, i) => ({ t, i })).sort((a, b) => (b.t.weight ?? 0) - (a.t.weight ?? 0) || a.i - b.i);
|
|
207
203
|
for (const { t } of sorted) {
|
|
@@ -226,8 +222,10 @@ class Machine {
|
|
|
226
222
|
finalized = false;
|
|
227
223
|
logger;
|
|
228
224
|
ident;
|
|
225
|
+
started = false;
|
|
226
|
+
tickTimer = null;
|
|
229
227
|
getAllStates() {
|
|
230
|
-
const logger =
|
|
228
|
+
const logger = this.logger?.child?.({ path: "machine.getAllStates" }) ?? this.logger;
|
|
231
229
|
logger?.info({ phase: "start" }, "Gathering all states...");
|
|
232
230
|
if (!this.initialState)
|
|
233
231
|
return [];
|
|
@@ -256,10 +254,9 @@ class Machine {
|
|
|
256
254
|
return new Machine;
|
|
257
255
|
}
|
|
258
256
|
finalize({
|
|
259
|
-
verbose,
|
|
260
257
|
ident
|
|
261
258
|
} = {}) {
|
|
262
|
-
const logger =
|
|
259
|
+
const logger = this.logger?.child?.({ path: "machine.finalize", instance: this.ident }) ?? this.logger;
|
|
263
260
|
if (!this.initialState || this.statesToCreate.length === 0) {
|
|
264
261
|
logger?.error({ phase: "error" }, "Finalization failed: no initial state or states to create");
|
|
265
262
|
throw new Error("Cannot finalize a machine without an initial state or states to create.");
|
|
@@ -269,13 +266,11 @@ class Machine {
|
|
|
269
266
|
} else {
|
|
270
267
|
this.ident = randomUUID();
|
|
271
268
|
}
|
|
272
|
-
if (verbose)
|
|
273
|
-
glogger = pino();
|
|
274
|
-
logger?.info("Logging enabled.");
|
|
275
269
|
logger?.info({ phase: "start" }, `Finalizing machine ${this.ident}...`);
|
|
276
270
|
const registry = new Map;
|
|
277
271
|
logger?.info({ phase: "progress" }, `Building state registry...`);
|
|
278
272
|
for (const s of this.statesToCreate) {
|
|
273
|
+
s.logger = this.logger;
|
|
279
274
|
if (!s.ident) {
|
|
280
275
|
logger?.error({ phase: "error" }, "Finalization failed: state missing ident");
|
|
281
276
|
throw new Error("State missing ident.");
|
|
@@ -307,7 +302,7 @@ class Machine {
|
|
|
307
302
|
return this;
|
|
308
303
|
}
|
|
309
304
|
addState(state, options = {}) {
|
|
310
|
-
const logger =
|
|
305
|
+
const logger = this.logger?.child?.({ path: "machine.addState", instance: this.ident }) ?? this.logger;
|
|
311
306
|
logger?.info({ phase: "start", state: state.ident, isInitial: !!options.initial }, "Adding state");
|
|
312
307
|
this.statesToCreate.push(state);
|
|
313
308
|
if (options.initial) {
|
|
@@ -316,45 +311,29 @@ class Machine {
|
|
|
316
311
|
logger?.info({ phase: "end", state: state.ident }, "State added");
|
|
317
312
|
return this;
|
|
318
313
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
await this.currentState.playlist.run(stateData);
|
|
336
|
-
logger?.info({ phase: "progress", state: this.currentState.ident }, "Initial playlist run complete.");
|
|
337
|
-
}
|
|
338
|
-
const tick = async () => {
|
|
339
|
-
const tickLogger = logger?.child({ path: "machine.tick" });
|
|
340
|
-
tickLogger?.info({ phase: "start", state: this.currentState.ident }, "Tick.");
|
|
341
|
-
const next = await this.currentState.next(stateData);
|
|
342
|
-
if (next) {
|
|
343
|
-
tickLogger?.info({ phase: "progress", from: this.currentState.ident, to: next.ident }, "Transitioning state.");
|
|
344
|
-
this.currentState = next;
|
|
345
|
-
await this.currentState.playlist.run(stateData);
|
|
346
|
-
tickLogger?.info({ phase: "progress", state: this.currentState.ident }, "Playlist run complete.");
|
|
347
|
-
} else {
|
|
348
|
-
tickLogger?.info({ phase: "progress", state: this.currentState.ident }, "No next state.");
|
|
314
|
+
addLogger(logger) {
|
|
315
|
+
this.logger = logger;
|
|
316
|
+
if (this.initialState) {
|
|
317
|
+
const visited = new Set;
|
|
318
|
+
const stack = [this.initialState];
|
|
319
|
+
while (stack.length > 0) {
|
|
320
|
+
const node = stack.pop();
|
|
321
|
+
if (!node.ident || visited.has(node.ident))
|
|
322
|
+
continue;
|
|
323
|
+
visited.add(node.ident);
|
|
324
|
+
node.logger = logger;
|
|
325
|
+
for (const tr of node.transitions || []) {
|
|
326
|
+
if (tr.to && tr.to.ident && !visited.has(tr.to.ident)) {
|
|
327
|
+
stack.push(tr.to);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
349
330
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
tick();
|
|
353
|
-
logger?.info({ phase: "end" }, "Machine started, tick loop initiated.");
|
|
331
|
+
}
|
|
332
|
+
return this;
|
|
354
333
|
}
|
|
355
|
-
async run(stateData) {
|
|
356
|
-
const logger =
|
|
357
|
-
logger?.info({ phase: "start" }, "Running machine...");
|
|
334
|
+
async run(stateData, options) {
|
|
335
|
+
const logger = this.logger?.child?.({ path: "machine.run", instance: this.ident }) ?? this.logger;
|
|
336
|
+
logger?.info({ phase: "start", options }, "Running machine...");
|
|
358
337
|
if (!this.finalized) {
|
|
359
338
|
logger?.error({ phase: "error" }, "Machine not finalized");
|
|
360
339
|
throw new Error("Cannot run a machine that is not finalized.");
|
|
@@ -365,27 +344,39 @@ class Machine {
|
|
|
365
344
|
}
|
|
366
345
|
const allStates = this.getAllStates();
|
|
367
346
|
const visitedIdents = new Set;
|
|
347
|
+
let transitionsCount = 0;
|
|
348
|
+
if (options.stopAfter !== undefined && options.stopAfter === 0) {
|
|
349
|
+
logger?.info({ phase: "end", reason: "stopAfter", count: transitionsCount }, "Stop condition met.");
|
|
350
|
+
return stateData;
|
|
351
|
+
}
|
|
368
352
|
let current = this.initialState;
|
|
369
353
|
logger?.info({ phase: "progress", state: current.ident }, "Set initial state. Running playlist.");
|
|
370
354
|
await current.playlist.run(stateData);
|
|
355
|
+
transitionsCount = 1;
|
|
371
356
|
visitedIdents.add(current.ident);
|
|
357
|
+
if (options.stopAfter !== undefined && transitionsCount >= options.stopAfter) {
|
|
358
|
+
logger?.info({ phase: "end", reason: "stopAfter", count: transitionsCount }, "Stop condition met.");
|
|
359
|
+
return stateData;
|
|
360
|
+
}
|
|
372
361
|
while (true) {
|
|
373
362
|
if (!current.transitions || current.transitions.length === 0) {
|
|
374
|
-
|
|
375
|
-
|
|
363
|
+
if (options.mode !== "infinitely") {
|
|
364
|
+
logger?.info({ phase: "end", state: current.ident, reason: "leaf" }, "Stop condition met.");
|
|
365
|
+
return stateData;
|
|
366
|
+
}
|
|
376
367
|
}
|
|
377
368
|
let next = await current.next(stateData);
|
|
378
369
|
if (!next) {
|
|
379
370
|
const retryDelay = current.retry;
|
|
380
|
-
if (
|
|
381
|
-
logger?.info({ phase: "end", state: current.ident }, "
|
|
371
|
+
if (retryDelay === false) {
|
|
372
|
+
logger?.info({ phase: "end", state: current.ident, reason: "no-transition-no-retry" }, "Stop condition met.");
|
|
382
373
|
return stateData;
|
|
383
374
|
}
|
|
384
375
|
let retries = 0;
|
|
385
376
|
logger?.info({ phase: "progress", state: current.ident, retryDelay }, "No next state, beginning retry logic.");
|
|
386
377
|
while (!next) {
|
|
387
|
-
if (current.maxRetries && retries >= current.maxRetries) {
|
|
388
|
-
logger?.warn({ phase: "end", state: current.ident, retries }, "
|
|
378
|
+
if (current.maxRetries !== false && retries >= current.maxRetries) {
|
|
379
|
+
logger?.warn({ phase: "end", state: current.ident, retries, reason: "retries-exhausted" }, "Stop condition met.");
|
|
389
380
|
return stateData;
|
|
390
381
|
}
|
|
391
382
|
await this.sleep(retryDelay);
|
|
@@ -400,17 +391,29 @@ class Machine {
|
|
|
400
391
|
}
|
|
401
392
|
const resolvedNext = next;
|
|
402
393
|
if (resolvedNext === this.initialState) {
|
|
403
|
-
|
|
404
|
-
|
|
394
|
+
if (options.mode !== "infinitely") {
|
|
395
|
+
logger?.info({ phase: "end", reason: "roundtrip" }, "Stop condition met.");
|
|
396
|
+
return stateData;
|
|
397
|
+
}
|
|
405
398
|
}
|
|
406
399
|
logger?.info({ phase: "progress", from: current.ident, to: resolvedNext.ident }, "Transitioning state.");
|
|
407
400
|
current = resolvedNext;
|
|
408
401
|
await current.playlist.run(stateData);
|
|
409
402
|
visitedIdents.add(current.ident);
|
|
410
|
-
|
|
411
|
-
|
|
403
|
+
transitionsCount++;
|
|
404
|
+
if (options.stopAfter !== undefined && transitionsCount >= options.stopAfter) {
|
|
405
|
+
logger?.info({ phase: "end", reason: "stopAfter", count: transitionsCount }, "Stop condition met.");
|
|
412
406
|
return stateData;
|
|
413
407
|
}
|
|
408
|
+
if (visitedIdents.size >= allStates.length) {
|
|
409
|
+
if (options.mode === "any") {
|
|
410
|
+
logger?.info({ phase: "end", reason: "all-visited" }, "Stop condition met.");
|
|
411
|
+
return stateData;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (options.mode === "infinitely") {
|
|
415
|
+
await this.sleep(options.interval ?? 1000);
|
|
416
|
+
}
|
|
414
417
|
}
|
|
415
418
|
}
|
|
416
419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fkws/klonk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "A lightweight, extensible workflow automation engine for Node.js and Bun",
|
|
5
5
|
"repository": "https://github.com/klar-web-services/klonk",
|
|
6
6
|
"homepage": "https://klonk.dev",
|
|
@@ -48,12 +48,11 @@
|
|
|
48
48
|
"@types/node": "latest",
|
|
49
49
|
"@vitest/coverage-v8": "^4.0.3",
|
|
50
50
|
"bunup": "^0.6.2",
|
|
51
|
-
"vitest": "^4.0.3"
|
|
51
|
+
"vitest": "^4.0.3",
|
|
52
|
+
"pino": "^10.1.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
55
|
"typescript": "^5"
|
|
55
56
|
},
|
|
56
|
-
"dependencies": {
|
|
57
|
-
"pino": "^10.1.0"
|
|
58
|
-
}
|
|
57
|
+
"dependencies": {}
|
|
59
58
|
}
|