@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 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` can be run synchronously to completion (`run`) for request/response style work, or started as a long-running background process (`start`).
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
- .finalize({ // Finalize your machine to make it ready to run. Verbose machines emit JSON logs. If you don't provide an ident, a uuidv4 will be generated for it.
449
- verbose: true,
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 state
461
- // (leaf, failed, out of retries, looped back to initial state)
462
- // and returns the final state. The original state object is also mutated.
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 = glogger?.child({ path: "StateNode.next", state: this.ident });
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 = glogger?.child({ path: "machine.getAllStates" });
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 = glogger?.child({ path: "machine.finalize", instance: this.ident });
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 = glogger?.child({ path: "machine.addState", instance: this.ident });
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
- async start(stateData, options) {
374
- const logger = glogger?.child({ path: "machine.start", instance: this.ident });
375
- logger?.info({ phase: "start" }, "Starting machine...");
376
- if (!this.finalized) {
377
- logger?.error({ phase: "error" }, "Machine not finalized");
378
- throw new Error("Cannot start a machine that is not finalized.");
379
- }
380
- if (!this.initialState) {
381
- logger?.error({ phase: "error" }, "No initial state");
382
- throw new Error("Cannot start a machine without an initial state.");
383
- }
384
- const interval = options?.interval ?? 1000;
385
- logger?.info({ phase: "progress", interval }, "Machine interval set");
386
- if (!this.currentState) {
387
- this.currentState = this.initialState;
388
- logger?.info({ phase: "progress", state: this.currentState.ident }, "Set initial state. Running playlist.");
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
- setTimeout(tick, interval);
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 = glogger?.child({ path: "machine.run", instance: this.ident });
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
- logger?.info({ phase: "end", state: current.ident }, "Reached leaf node, run complete.");
429
- return stateData;
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 (!retryDelay) {
435
- logger?.info({ phase: "end", state: current.ident }, "No next state and retries disabled, run complete.");
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 }, "Retry limit exhausted, run complete.");
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
- logger?.info({ phase: "end" }, "Next state is initial state, run complete.");
458
- return stateData;
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
- if (visitedIdents.size >= allStates.length) {
465
- logger?.info({ phase: "end" }, "All reachable states visited, run complete.");
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
- import { Logger } from "pino";
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({ verbose, ident }?: {
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
- * Start the machine in a repeated tick loop. Executes the current state's playlist,
468
- * evaluates transitions, and schedules the next tick with `setTimeout`.
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
- start(stateData: TStateData, options?: {
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
- export { Workflow, TriggerEvent, Trigger, Task, StateNode, Railroad, Playlist, Machine2 as Machine };
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 = glogger?.child({ path: "StateNode.next", state: this.ident });
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 = glogger?.child({ path: "machine.getAllStates" });
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 = glogger?.child({ path: "machine.finalize", instance: this.ident });
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 = glogger?.child({ path: "machine.addState", instance: this.ident });
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
- async start(stateData, options) {
320
- const logger = glogger?.child({ path: "machine.start", instance: this.ident });
321
- logger?.info({ phase: "start" }, "Starting machine...");
322
- if (!this.finalized) {
323
- logger?.error({ phase: "error" }, "Machine not finalized");
324
- throw new Error("Cannot start a machine that is not finalized.");
325
- }
326
- if (!this.initialState) {
327
- logger?.error({ phase: "error" }, "No initial state");
328
- throw new Error("Cannot start a machine without an initial state.");
329
- }
330
- const interval = options?.interval ?? 1000;
331
- logger?.info({ phase: "progress", interval }, "Machine interval set");
332
- if (!this.currentState) {
333
- this.currentState = this.initialState;
334
- logger?.info({ phase: "progress", state: this.currentState.ident }, "Set initial state. Running playlist.");
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
- setTimeout(tick, interval);
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 = glogger?.child({ path: "machine.run", instance: this.ident });
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
- logger?.info({ phase: "end", state: current.ident }, "Reached leaf node, run complete.");
375
- return stateData;
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 (!retryDelay) {
381
- logger?.info({ phase: "end", state: current.ident }, "No next state and retries disabled, run complete.");
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 }, "Retry limit exhausted, run complete.");
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
- logger?.info({ phase: "end" }, "Next state is initial state, run complete.");
404
- return stateData;
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
- if (visitedIdents.size >= allStates.length) {
411
- logger?.info({ phase: "end" }, "All reachable states visited, run complete.");
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.12",
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
  }