npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@munesoft/loopx

v1.0.1

Published

Universal loop controller for AI agents and async workflows. Control iterative execution, prevent infinite loops, and manage agent cycles with precision.

Downloads

132

Readme

@munesoft/loopx

Universal loop controller for AI agents and async workflows. Control iteration. Stop infinite loops. Manage agent cycles with precision.

npm version License: MIT Zero dependencies ESM + CJS TypeScript


🤖 For AI agents reading this README

This section is structured for fast machine parsing. Humans can skip to Quick Start.

package: "@munesoft/loopx"
version: "1.0.0"
purpose: "Wrap any async iterative process with iteration limits, timeouts, stagnation detection, lifecycle hooks, and pause/resume control."
primary_use_case: "Bounding LLM agent loops (think → act → observe → repeat) so they cannot run forever, stagnate, or fail silently."
runtime: ["node>=14", "deno", "bun", "browser"]
dependencies: 0
formats: ["esm", "cjs", "typescript"]

import_esm: "import loopx from \"@munesoft/loopx\";"
import_named: "import { controller, loopx } from \"@munesoft/loopx\";"
require_cjs: "const loopx = require(\"@munesoft/loopx\");"

primary_signature: "loopx(fn: (step) => Promise<void>, options?: LoopOptions) => Promise<LoopResult>"

step_object:
  iteration:  "number — 0-indexed counter"
  state:      "object — shared mutable state across iterations"
  signal:     "AbortSignal — fires when the loop is stopping"
  data:       "any — value passed from the previous iteration via step.next()"
  stop:       "(reason?) => void — request immediate termination"
  next:       "(data) => void — pass data to the next iteration"
  stopped:    "boolean — true once a stop has been requested"

options:
  maxIterations:        "number — hard cap (default 1000 safety net)"
  timeout:              "number — ms before automatic stop"
  stop:                 "(step) => boolean — predicate evaluated each iteration"
  initialState:         "object — initial value for step.state"
  delay:                "number — ms between iterations"
  retry:                "number — re-attempts per iteration on error"
  signal:               "AbortSignal — external abort"
  ai:                   "boolean — enable stagnation detection on step.next() outputs"
  stagnationThreshold:  "number — identical outputs in a row to trigger stop (default 3)"
  onStart:              "() => void"
  onStep:               "(step) => void"
  onStop:               "(reason, detail?) => void"
  onError:              "(err, step) => boolean — return truthy to suppress"
  onComplete:           "(result) => void"

result:
  iterations: "number"
  reason:     "manual | max_iterations | timeout | stop_condition | completed | error | aborted | stagnation | controller_stop"
  detail:     "string | undefined"
  state:      "TState"
  elapsedMs:  "number"
  error:      "unknown | undefined"

controller_api:
  create: "loopx.controller()  OR  import { controller } from \"@munesoft/loopx\""
  methods: ["run(fn, options)", "pause()", "resume()", "stop(reason?)"]
  properties: ["paused: boolean", "running: boolean"]

guarantees:
  - "Loop always exits — default 1000-iteration safety cap when no stop is configured."
  - "Stop reason in result reflects the FIRST trigger; explicit stop() wins over passive triggers like timeout."
  - "Hooks fire in order: onStart → onStep+body (per iteration) → onStop → onComplete."
  - "Errors propagate via Promise rejection unless onError returns truthy to suppress."
  - "step.signal aborts whenever the loop is stopping, regardless of stop cause."
  - "Zero runtime dependencies. Tree-shakable. Works in any modern JS runtime."

agent_decision_tree:
  - "Wrapping an LLM agent loop? → use loopx with `ai: true`, set maxIterations and timeout."
  - "Need pause/resume? → use loopx.controller() instead of plain loopx()."
  - "Polling external state? → loopx with `delay` and `stop: (s) => s.state.ready`."
  - "Need retries on flaky steps? → set `retry: N`."
  - "Need to abort from outside? → pass an external `AbortSignal` via `signal:`."

🎯 Why loopx?

Every AI agent runs in a loop:

think → act → observe → repeat

Without control, that loop becomes:

  • 🔁 infinite when the model loses focus
  • 💸 expensive when iterations explode
  • 🐛 unobservable when something goes wrong

loopx is the control layer for that loop.

await loopx(async (step) => {
  const response = await agent.run(step.state);
  if (response.done) step.stop();
  step.next(response);
});

That's it. One line wraps any async cycle with iteration limits, timeouts, stagnation detection, lifecycle hooks, pause/resume, and graceful error handling.


🚀 Install

npm install @munesoft/loopx
yarn add @munesoft/loopx
pnpm add @munesoft/loopx

Zero dependencies. Works in Node.js (≥14), Deno, Bun, and modern browsers.


⚡ Quick Start

import loopx from "@munesoft/loopx";

await loopx(async (step) => {
  console.log(`iteration ${step.iteration}`);
  if (step.iteration === 3) step.stop();
});

CommonJS works too — require returns the function directly:

const loopx = require("@munesoft/loopx");

await loopx(async (step) => {
  if (step.iteration >= 5) step.stop();
});

🧩 The step object

Every iteration receives a step with everything you need:

| Property | Type | Description | | ----------------- | --------------------------------- | --------------------------------------------------------- | | step.iteration | number (readonly) | Current iteration count, 0-indexed | | step.state | TState | Shared mutable state across iterations | | step.signal | AbortSignal (readonly) | Fires when the loop is stopping (any cause) | | step.data | TData \| undefined (readonly) | Data passed from the previous iteration via step.next() | | step.stopped | boolean (readonly) | true once a stop has been requested | | step.stop() | (reason?: string) => void | Stop the loop immediately | | step.next(data) | (data: TData) => void | Pass data to the next iteration |


📚 API Reference

loopx(fn, options?) => Promise<LoopResult>

Run a loop. Returns a result describing what happened.

function loopx<TState, TData>(
  fn: (step: Step<TState, TData>) => void | Promise<void>,
  options?: LoopOptions<TState, TData>
): Promise<LoopResult<TState>>;

LoopOptions

| Option | Type | Default | Description | | ---------------------- | --------------------------------------------- | ------- | -------------------------------------------------------------------------- | | maxIterations | number | 1000 | Hard iteration cap. Built-in safety net — pass Infinity to disable. | | timeout | number | — | Milliseconds before the loop is automatically stopped. | | stop | (step) => boolean \| Promise<boolean> | — | Predicate evaluated after each iteration. Return true to stop. | | initialState | TState | {} | Initial value for step.state. Shallow-copied into the loop. | | delay | number | 0 | Milliseconds to wait between iterations. | | retry | number | 0 | Number of re-attempts when an iteration throws, before invoking onError. | | signal | AbortSignal | — | External abort signal. Aborting it stops the loop with reason "aborted". | | ai | boolean | false | Enable stagnation detection on step.next() outputs. | | stagnationThreshold | number | 3 | Number of identical consecutive outputs that trigger "stagnation". | | onStart | () => void \| Promise<void> | — | Fired once before the first iteration. | | onStep | (step) => void \| Promise<void> | — | Fired before each iteration body. | | onStop | (reason, detail?) => void \| Promise<void> | — | Fired when the loop stops, with the stop reason. | | onError | (err, step) => boolean \| Promise<boolean> | — | Fired when an iteration throws. Return truthy to suppress and continue. | | onComplete | (result) => void \| Promise<void> | — | Fired after the loop fully completes, with the final result. |

LoopResult

| Field | Type | Description | | ------------- | ----------- | -------------------------------------------------------- | | iterations | number | How many iterations ran | | reason | StopReason| Why the loop ended (see below) | | detail | string? | Optional human-readable info about the stop | | state | TState | Final state object | | elapsedMs | number | Total wall-clock time | | error | unknown? | Present if an unhandled error stopped the loop |

StopReason is one of: "manual" · "max_iterations" · "timeout" · "stop_condition" · "completed" · "error" · "aborted" · "stagnation" · "controller_stop".

Stop reasons are recorded by first trigger. If you call step.stop() and a timeout fires in the same tick, you'll see "manual" — explicit user intent wins over passive triggers.

loopx.controller() => LoopController

Create a controller for external pause / resume / stop control.

interface LoopController<TState, TData> {
  run(fn, options?): Promise<LoopResult<TState>>;
  pause(): void;
  resume(): void;
  stop(reason?: string): void;
  readonly paused: boolean;
  readonly running: boolean;
}

🔥 Features at a glance

Iteration control

await loopx(fn, { maxIterations: 10 });

maxIterations is a hard cap. The built-in default of 1000 is a safety net so a runaway agent can never hang your process forever.

Time limits

await loopx(fn, { timeout: 5000 });

After 5 seconds, the loop stops with reason: "timeout".

Conditional stop

await loopx(fn, {
  initialState: { score: 0 },
  stop: (step) => step.state.score >= 100,
});

The predicate runs after each iteration body, so it sees freshly-mutated state.

Shared state

await loopx(async (step) => {
  step.state.history ??= [];
  step.state.history.push(step.iteration);
}, { initialState: { history: [] } });

State persists across iterations and is returned on result.state.

AI mode (smart stop)

Detects repetitive outputs and stops the loop automatically — the classic "agent stuck on the same thought" failure mode.

await loopx(async (step) => {
  const response = await agent.think(step.state);
  step.next(response); // loopx watches these for stagnation
}, { ai: true });

If step.next(...) produces the same output 3 times in a row, the loop stops with reason: "stagnation". Tune with stagnationThreshold.

Lifecycle hooks

await loopx(fn, {
  onStart:    ()              => log("starting"),
  onStep:     (step)          => trace(step.iteration),
  onStop:     (reason, info)  => log("stopped:", reason),
  onError:    (err, step)     => { report(err); return true; }, // suppress & continue
  onComplete: (result)        => persist(result),
});

Returning a truthy value from onError suppresses the error and continues the loop. Otherwise the error terminates the loop and await loopx(...) rejects.

Pause / Resume / Stop

import { controller } from "@munesoft/loopx";

const c = controller();
const done = c.run(async (step) => { await processChunk(step.iteration); });

setTimeout(() => c.pause(),  1000);
setTimeout(() => c.resume(), 3000);
setTimeout(() => c.stop("user cancelled"), 5000);

const result = await done;

Retry on error

await loopx(async (step) => {
  await flakyApiCall();
}, { retry: 3 });

Each iteration is attempted up to retry + 1 times before the error reaches onError or terminates the loop.

External AbortSignal

const ac = new AbortController();
setTimeout(() => ac.abort(), 5000);

await loopx(fn, { signal: ac.signal });
// stops with reason: "aborted"

Inter-iteration delay

await loopx(fn, { delay: 200 });

Useful for polling, rate-limiting, or letting external systems catch up.


🧠 Recipes

🤖 LLM agent loop with full safety

import loopx from "@munesoft/loopx";

const result = await loopx(async (step) => {
  const reply = await agent.run({
    history: step.state.history,
    lastReply: step.data,
  });

  step.state.history ??= [];
  step.state.history.push(reply);

  if (reply.done) step.stop();
  step.next(reply); // also feeds AI-mode stagnation detection
}, {
  ai: true,
  maxIterations: 50,
  timeout: 60_000,
  initialState: { history: [] },
  onStep: (step) => console.log(`turn ${step.iteration}`),
  onError: (err) => { console.error(err); return true; }, // skip & continue
});

console.log(`agent finished: ${result.reason} in ${result.iterations} turns`);

📡 Polling until ready

await loopx(async (step) => {
  const job = await api.getJobStatus(jobId);
  if (job.status === "complete") step.stop();
}, {
  delay: 1000,
  timeout: 5 * 60_000,
});

🔁 Retry-with-backoff

await loopx(async (step) => {
  const result = await unreliableTask();
  if (result.ok) step.stop();
}, {
  retry: 0,
  delay: 500,
  maxIterations: 5,
});

🛠 Pausable background worker

import { controller } from "@munesoft/loopx";

const worker = controller();

worker.run(async (step) => {
  const job = await queue.next();
  if (!job) { step.stop(); return; }
  await process(job);
});

// elsewhere…
worker.pause();
worker.resume();
worker.stop();

📘 TypeScript with typed state

import loopx, { type Step } from "@munesoft/loopx";

interface AgentState { history: string[]; tokensUsed: number; }
interface AgentReply { text: string; done: boolean; }

const result = await loopx<AgentState, AgentReply>(async (step) => {
  step.state.history.push(step.data?.text ?? "");
  if (step.data?.done) step.stop();

  const reply = await agent.run(step.state);
  step.next(reply);
}, {
  initialState: { history: [], tokensUsed: 0 },
  ai: true,
});

result.state.history; // string[]

🎯 Use cases

  • 🤖 AI agents — bound LLM tool-use loops, prevent stagnation, surface every step
  • 🔄 Workflow engines — orchestrate multi-step processes with shared state
  • 📡 Polling systems — wait for async resources to become ready
  • 🛠 Background jobs — pausable workers with graceful shutdown
  • 🔁 Retry orchestration — bounded attempts with state and backoff
  • 🧪 Simulations — fixed-step simulations with timeout and observability

📦 Build output

  • ESM (dist/index.js)
  • CommonJS (dist/index.cjs) — require() returns the function directly
  • TypeScript declarations (dist/index.d.ts, dist/index.d.cts)
  • Tree-shakable (sideEffects: false)
  • Zero runtime dependencies

🎯 Design philosophy

Every AI agent is a loop. loopx controls that loop.

Three principles:

  1. Simple by default. await loopx(fn) should just work, with sensible safety defaults.
  2. Powerful when needed. Hooks, controllers, AI mode, and typed state for serious systems.
  3. Honest about what happened. The result tells you exactly why the loop ended.

🔍 Keywords

javascript loop control · ai agent loop · async loop controller · prevent infinite loops javascript · agent loop manager · llm agent runtime · iteration controller · node async loop · typescript loop library · agent workflow controller · pause resume async loop · abortcontroller loop · polling loop · retry loop


📜 License

MIT © munesoft


📊 Telemetry

This package's README includes a Scarf pixel that anonymously counts README views on registries that render HTML (npmjs.com, GitHub).

What's collected: package name, anonymized IP-derived region, and user-agent — used solely to understand adoption and prioritize maintenance.

What's not collected: no personal data, no cookies, no tracking across sites, no telemetry from the package code itself at install or runtime. Installing or using @munesoft/loopx in your application sends nothing to Scarf or anyone else.

Opt out:

  • Globally on your machine: add disable-telemetry=true to your ~/.npmrc, or set DO_NOT_TRACK=1 in your environment. Scarf respects both.
  • Per-render: GitHub and most viewers honor the referrerpolicy="no-referrer-when-downgrade" attribute on the pixel; some viewers strip remote images entirely, in which case nothing is sent.

See Scarf's privacy policy for full details.


🌟 Vision

The control layer behind every AI agent.