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

loopengine

v0.3.61

Published

Runtime for tool-using AI agents with a transparent ReAct loop.

Readme

LoopEngine

CI

LoopEngine is a runtime for building AI agents: a persona, a set of tools, and permission rules, run through a transparent ReAct loop. No chain DSL, no hidden control flow — core/run-agent.ts is a single function you can read top to bottom. Run agents over a CLI, an HTTP API, or both.

Why LoopEngine

  • Up and running in one command. npx create-loopengine@latest scaffolds a real, standalone project — its own repo, a starter agent, CLI and HTTP adapters already wired up. npm install && npm run dev and you have a running agent server, not a half-finished template to wire up yourself.
  • A real Admin UI, not just logs. Open /agents/config in a browser to see and edit a live agent: its resolved config, permission rules, skills, tool connections, and secrets — no redeploy to change a rule or add a skill. See Admin UI below.
  • Human-in-the-loop, wired for real notification channels. A risky tool call or a genuinely ambiguous question routes to a human — live in a chat session, or durably via Slack, Lark, email, a generic webhook, or a polling queue, resumable minutes or days later. See Human in the loop below.
  • Sessions persist automatically. Send a message, get a reply, come back later with the same session id and continue — no database to wire up yourself, and it survives a crash mid-turn cleanly.
  • An ability system for reusable capabilities. Install a bundle of tools, a skill, and the permission rules that gate them with one command — real, reviewable files in your own repo, not an opaque dependency. See Ability system below.
  • Does real work, safely. Tools can hit a database, send emails, call GitHub/Slack, anything with an execute function. Permission rules gate what happens without a human: safe reads auto-run, risky actions get approval or are denied outright.
  • Multi-tenant and composable out of the box. The same deployed agent serves different customers with different permission levels, no forking required — and one agent's folder drops under another's subagents/ to become a tool the parent can delegate to, own tools/rules/loop intact.

So the payoff: you write one AgentConfig, and get a deployable, persistent, permission-safe service — not just a prompt-and-response demo.

Quick start

npx create-loopengine@latest my-agents
cd my-agents
npm install
cp .env.example .env   # fill in ANTHROPIC_API_KEY
npm run dev             # HTTP server on :8787 — same as `npx loopengine dev`

This scaffolds a standalone project — its own repo, with loopengine as a dependency, a starter agent, and CLI + HTTP adapters already wired up. That's the intended way to use LoopEngine: an independent project you own and build agents in, not a library you import into an existing app.

Define your first agent

npx loopengine add-agent weather-agent
# -> Created agents/weather-agent/index.ts

That's the fastest way to start: it generates agents/<name>/index.ts with a working AgentConfig stub, so you don't have to memorize the folder shape by hand. Or write it yourself — an agent is just an AgentConfig: name, systemPrompt, tools, rules, and a model. Drop it in a folder under agents/ — agents/<name>/index.ts — and it's picked up automatically, no registry to edit, no import to add.

Most fields don't even need to be written out — they default to a conventional path under the agent's own folder:

| Field | Defaults to | | --- | --- | | rules | agents/<name>/actauth.yml (missing → deny everything) | | tools | agents/<name>/tools/index.ts's exported tools (missing → no tools) | | skillsDirs | agents/<name>/skills (missing → no skills) |

There's no subagents field to omit — agents/<name>/subagents/* is its own folder convention (drop another agent's folder there and it becomes one of this agent's tools automatically), always merged into tools on top of whatever the table above resolves to.

rules is how you gate what a tool can do without approval — each rule maps a scope (tenant/environment) + tool name to allow/ask/deny, with an ask decision routing to a human — see Human in the loop below.

The simplest possible agent, agents/weather-agent/index.ts — tools, rules, and skillsDirs all omitted, so they default to agents/weather-agent/tools/, actauth.yml, and skills/:

import type { AgentConfig } from '../../agent-config.js'

export const config: AgentConfig = {
  name: 'weather-agent',
  systemPrompt: 'You answer questions about the weather.',
  model: { provider: 'anthropic', model: 'claude-sonnet-5-5' }, // reads ANTHROPIC_API_KEY
}

See agents/customer-service/ and agents/file-agent/ for complete, working examples.

Admin UI

Run the HTTP adapter and open http://localhost:8787/agents — one console for every registered agent: a sidebar of agents (with a badge when one still needs a setting), and for each agent a set of tabs. No code changes or redeploy needed for most of it:

| Tab | What you can do | | --- | --- | | Chat | Talk to the agent, approve or deny tool calls as they come up, and follow each step (model calls, tool calls, permission checks) in the Run trace. Past conversations are under History. | | Overview | A card per tab (tools, permissions, skills, settings), plus the system prompt, model, hooks and limits, each editable in place. | | Abilities | Install, upgrade, and uninstall abilities (see Ability system). | | Tools | Your hand-written tools, tools from integrations (e.g. Composio) and subagents, with whether each call runs on its own, asks first, or is denied. Build an HTTP tool or connect an integration without touching a file. | | Skills | Create, edit, and delete SKILL.md files for this agent directly in the browser — write the body, preview the rendered markdown, save. | | Permissions | Add, edit, and delete permission rules (actauth) — who, tool, decision — and choose what happens when no rule matches. | | Schedules | One of the agent's workflows (with values for its parameters), or a custom message, run on a timer — once at a set time, or every N minutes/hours/days. A workflow schedule starts each run with the workflow's start message and steps as they are then. Run now, pause, resume, cancel, see each run's result, and optionally send results through the agent's notifier. | | Environment | Every setting an installed ability reads, with its project-wide value and this agent's own value side by side, and which one the agent actually uses. A secret is never shown again once set. |

Models and Integrations in the sidebar show which model providers have a key set and which outside tool sources are connected. Tabs deep-link: /agents/config?agent=<name>&tab=tools, /playground?agent=<name> for Chat.

Every tab is backed by a real API (GET /agents/:name/config, .../actauth, .../env, ...) that reuses the exact same resolution runAgent() itself uses — so what you see here can't drift out of sync with what a real request actually gets, and never returns a model API key.

Human in the loop

A risky tool call (actauth's ask decision) or a genuinely ambiguous question the model itself raises both need a human — and LoopEngine handles both the same way, live or durable:

  • Live — someone's actively watching right now (a terminal, an open chat session). The turn just waits; cli/http_stream get this automatically, with nothing to configure.
  • Durable — nobody's watching (a webhook-triggered run, a ticket that came in overnight). The turn ends immediately with a resumable pending state, and a real notification goes out on whichever channel you've configured — resolvable minutes or days later without holding any process open in between.

Six channels ship as real, working notifiers — configure one and the "send a notification" side is done for you:

| Channel | What it does | | --- | --- | | webhook | Signed HMAC-SHA256 POST to any URL you own | | slack | An interactive message via chat.postMessage, with Approve/Deny buttons | | lark | A Lark/Feishu interactive card | | email | An email per decision, through your own sendEmail: with signed, expiring magic links to your endpoints, or a link to this server's decision page | | database | Writes a row for your own worker/dashboard to poll | | redis | Pushes a queue entry for your own worker to consume |

export const config: AgentConfig = {
  // ...
  httpNotifier: {
    channel: 'slack',
    config: { botToken: process.env.SLACK_BOT_TOKEN!, channelId: process.env.SLACK_CHANNEL_ID! },
    events: ['approval', 'question'],
  },
}

Or skip the code: in the console, the agent's Messaging tab sets a webhook, Slack, Lark or email target for one agent (email sends through Resend, SendGrid or Postmark with your API key, each email linking to the decision page) (saved to agents/<name>/messaging.yml, with its secret in the agent's own .env; an older notifier.yml is still read), sends a test and shows recent deliveries. With Slack, people can also reply in a request's thread to ask the agent about it, and it answers there (Answer replies in a request's thread, on by default). Approvers, picked from the channel's members, can also decide a request by replying ("yes, go ahead", "no", or the answer to a question); a separate model call reads only their own message to tell whether it's a decision, and anything unclear is treated as a question. An approver can also change a request first ("make it $40"): the change is checked against the tool's schema and the permission rules and shown with Approve as changed / Keep original / Deny, and nothing runs until they approve it. A plain "yes" ("approve", "go ahead", "好的", "はい", "sí", 👍…: the whole message, matched against a fixed list of phrases in common languages, not by a model) runs it; any other reply read as approval ("sure, send it", "yes but check the amount") shows exactly what will run with a Confirm button, and only that click runs it. Arguments are shown as labelled fields, with long text such as an email body quoted, not as JSON. Once approvers are listed, only their button clicks count too. The same tab lists every decision still waiting with Approve/Deny buttons (its count shows on the tab), and warns you when an agent has ask rules but nobody would be told.

Not sure what to automate? The console's Find your first agent (/interview, linked from the sidebar and the Agents page) is a short interview about your work, in any language. It suggests up to three agents worth building, ranked by the time they'd save, and creates the one you pick: its instructions, permissions where every tool asks you first, and a paused schedule if it runs on a timer, plus a checklist of what's left to connect. The interviewer runs on the model of one of your agents, can't see your systems, and creates nothing until you click.

Or skip the interview: Templates (/templates, linked from the sidebar and the Agents page) are ready-made agents: answer customer emails, refund approvals, triage incidents, check expense claims, lead follow-up, inbox to tasks, and a weekly research digest. Preview one's instructions and setup checklist, then create it the same way, as many times as you like. Templates live in core/system-agents/templates.ts, in the same shape the interview suggests.

Every notification links to a decision page: a signed, single-use link (/decide/<token>, valid 7 days) where whoever got the message can approve, deny, edit the arguments or answer a question, with no login and no app of your own. So the quickest setup is Slack: create the Slack app from the manifest the console shows, paste its bot token, pick a channel from the list, save; each approval arrives with an "Open to approve or deny" link. Add the app's signing secret too and the message also gets Approve/Deny buttons, handled by LoopEngine itself at /hooks/slack/<agent>, with no handler to deploy. Lark cards carry the same link. Links and buttons need the server to be reachable from where people click (a public HTTPS address or a tunnel).

For a third-party app calling an agent over plain HTTP:

  1. POST /agents/<name>/messages → when a call needs approval, the reply has stopReason: "pending_approval" and a pending list, each item with its pendingId and resolvePath.
  2. The webhook carries the same pendingId, resolvePath (and a full resolveUrl when LOOPENGINE_PUBLIC_URL is set), signed with X-Actauth-Signature. Failed deliveries are retried.
  3. POST <resolvePath> with {"decision":"approve"} (or "deny") runs the tool, resumes the turn, and returns the agent's reply. GET /agents/<name>/pending lists what's waiting, for apps that poll instead of receiving webhooks.

Reference implementations for the receiving side (verifying a Slack click, a webhook HMAC signature, a magic-link token) ship under examples/notifier-handler/ for the four channels that need one. See HUMAN_IN_THE_LOOP.md for the full setup guide — live and durable, worked examples, and how resumption actually works under the hood.

Scheduled tasks

The server runs one scheduler (core/scheduler.ts) that sends a message to an agent later — once (run_at) or on an interval (every: "1h"), optionally from a set first run (start_at), which every later run keeps: every: "1d" from a 9:00 start runs daily at 9:00, every: "7d" from a Monday runs on Mondays. at: "09:00" (plus weekday: "monday") picks that start for you. Times of day are in the workspace's time zone, chosen in the console's Settings (LOOPENGINE_TIME_ZONE; the server's own when unset): a timestamp without an offset is read there, and daily and weekly tasks keep their time of day when the clocks change. Schedule from the console's Schedules tab, the GET/POST /agents/<name>/schedules API, or the agent itself: every agent has built-in system_schedule_task, system_list_scheduled_tasks, system_check_scheduled_task and system_cancel_scheduled_task tools, so "remind me tomorrow at 9" just works. An agent can only schedule, see or cancel its own tasks, up to 25 active at a time (LOOPENGINE_SCHEDULER_MAX_PER_AGENT); selfScheduling: false in its config removes the tools. Tasks left by the old lp-task-scheduler ability are imported at start. Each run goes through the same path as an HTTP call: an approval goes wherever the Permissions tab sends it (and resumes the run once decided); with nowhere set, a call that needs approval is denied. Intervals keep their original slot (an hourly task created at 10:07 stays on :07); a run missed while the server was down fires once when it's back. Tasks live in .scheduler/ (or LOOPENGINE_SCHEDULER_DIR); the tick is LOOPENGINE_SCHEDULER_TICK_MS (10 s); LOOPENGINE_SCHEDULER=off turns the loop off for this process. Behind a hosting platform, GET /internal/activity reports nextWakeAt and POST /internal/scheduler/tick fires whatever is due, so a stopped server can be woken in time.

Ability system

Giving an agent a new capability is usually three separate, hand-authored things: a tool file, a SKILL.md teaching the model when to use it, and an actauth rule allowing it to actually run. A loopengine ability bundles all three into one installable unit:

npx loopengine add-ability <spec> --agent customer-service

<spec> is anything npm pack understands — a public or private npm package, a scoped package on a private registry, or a plain git repo. The tool files, skill directory, and actauth rules all land as real files in the agent's own tree, copied in rather than imported as a node_modules dependency — reviewable, diffable, and editable the same as anything you would have hand-written, not an opaque black box.

npx loopengine upgrade-ability <abilityName> --agent customer-service
npx loopengine remove-ability <abilityName> --agent customer-service

Upgrading does a real three-way merge per file — a hand-edit since install survives, a genuine conflict leaves <<<<<<< markers to resolve by hand instead of silently overwriting either side. Removing refuses a file that's been modified since install unless you pass --force. An ability can also declare the env vars its tools need, which then show up in the Admin UI's Environment tab automatically once installed. See ABILITIES.md for the full design — format, publishing, and how the merge/upgrade mechanics work in detail.

Running an agent

adapters/cli.ts and adapters/http.ts are files your own scaffolded project owns (see "Quick start" above), not something hidden inside the loopengine package — run them directly with tsx, or through shorter loopengine commands that just call the same files for you:

npx loopengine run customer-service --session s1 "order A-1001 arrived broken"
npx loopengine run customer-service --session s1 --input "order A-1001 arrived broken"
npx loopengine serve   # HTTP server on :8787
npx loopengine dev     # same server, restarts on file changes (tsx watch)

--session <id> is just an arbitrary string you pick — s1 above isn't special, it's only a label. Omit it entirely for a fresh, one-off conversation each call; reuse the same value on a later call to continue that exact conversation (the id is printed to stderr if you omit it, so you can capture it for next time). --input "<message>" is an alternative to the trailing positional message, for scripts that build the argument list programmatically and would rather not depend on the message always being the last argument.

run/serve/dev are thin wrappers, not a separate implementation — each shells out to your project's own adapters/cli.ts or adapters/http.ts via npx tsx (so it's always your copy that runs, edits included, resolved from your project's own node_modules), and fails with a clear message pointing at create-loopengine if that file doesn't exist yet. .env is loaded automatically for all three, equivalent to passing --env-file-if-exists=.env to tsx yourself.

The same thing, spelled out without the wrapper:

CLI:

npx tsx adapters/cli.ts --agent customer-service --session s1 "order A-1001 arrived broken"

HTTP:

npx tsx --env-file-if-exists=.env adapters/http.ts
curl -X POST localhost:8787/agents/customer-service/messages \
  -H 'content-type: application/json' \
  -d '{"customerEmail":"[email protected]","message":"order A-1001 arrived broken"}'

Add /stream to the URL for a Server-Sent Events response — one event per loop step (tool call, permission decision, budget check) instead of a single reply at the end.

Three small browser pages, all cross-linked, share one look (adapters/dev-ui-styles.ts), and never need a build step — each is a self-contained HTML string served straight out of adapters/http.ts:

Agents list: open http://localhost:8787/agents in a browser (the same route returns plain {agents: [...]} JSON to a non-browser client — content-negotiated on the Accept header, so nothing that already calls it as an API needs to change) to see every registered agent with links into the playground and config page below.

Chat: open http://localhost:8787/playground (optionally ?agent=<name> to preselect one) — the console's Chat tab: talk to an agent and watch the loop-step event stream render live in the Run trace instead of reading raw SSE frames. Same /messages/stream route underneath.

Building your own client: the playground is one UI on top of a typed event protocol, not the only way to consume a turn — see PROTOCOL.md for the full LoopEvent catalog, the SSE/ plain-HTTP/CLI transport bindings, and a JSON Schema (protocol/loop-event.schema.json) any language can validate against. core/client.ts and the examples/chatbox/react/examples/chatbox/vue hooks are reference implementations of it.

Agent config page: this is the Admin UI described above — open http://localhost:8787/agents/config (optionally ?agent=<name>).

Wiring a real model

Declare AgentConfig.model and the runtime builds a real ModelCall for you, using the matching API key from the environment:

| provider | env var | model required? | | --- | --- | --- | | 'anthropic' | ANTHROPIC_API_KEY | no — defaults to claude-sonnet-5-5 | | 'openai' | OPENAI_API_KEY | yes | | 'deepseek' | DEEPSEEK_API_KEY | yes | | 'kimi' | MOONSHOT_API_KEY | yes | | 'glm' | GLM_API_KEY | yes | | 'gemini' | GEMINI_API_KEY | yes | | 'xai' | XAI_API_KEY | yes | | 'meta' | META_API_KEY | yes |

'kimi''s env var is MOONSHOT_API_KEY, not KIMI_API_KEY — deliberate, matching Moonshot AI's own docs (the API/company behind Kimi) rather than this package's own provider name. 'meta' (Meta's Muse models, e.g. muse-spark-1.3, through the Meta Model API) reads META_API_KEY, LiteLLM's name, rather than the generic MODEL_API_KEY in Meta's own examples. openai/deepseek/kimi/glm/gemini/xai/meta all reuse the same OpenAI-Chat-Completions-compatible request translation, just pointed at each provider's own base URL — see core/model-calls/*.ts for the provider-specific details (base URLs, max_completion_tokens vs max_tokens, and Gemini's own "still in beta" caveat on Google's compatibility layer).

The console's model dropdown (New agent, and an agent's Model > Edit) lists each provider's models live from its API once its key is saved. To choose the list yourself, add a models.yaml next to .env mapping each provider to its models; a provider listed there offers exactly those, in that order, with no key needed, and edits show the next time the dropdown loads (LOOPENGINE_MODELS_FILE points at a different file):

anthropic:
  - claude-opus-5-5
  - claude-sonnet-5-5
deepseek:
  - deepseek-v4-pro

For anything else (a custom SDK client, a canned/simulated model for testing), export your own createModelCall(): ModelCall instead.

Built on

| Package | Responsibility | |---|---| | actauth | Permission gating (allow/ask/deny/pending) with live and durable human-approval hooks | | core/budget.ts / core/compaction.ts | Context-window budget tracking and tail-preserving compaction (vendored in-repo, not an external dependency) | | core/recovery.ts | Retries prompt-too-long / truncated-output failures (vendored in-repo, not an external dependency) | | core/durable-approvals.ts | TurnCheckpoint/CheckpointStore (file/Redis) backing durable, resumable ask decisions — see HUMAN_IN_THE_LOOP.md | | core/sessionknit.ts | Durable session log with crash-interruption detection (vendored in-repo, not an external dependency) | | core/skillgarden/ | SKILL.md discovery and lazy loading (vendored in-repo, not an external dependency) | | core/toollane.ts | Parallel/solo tool-call scheduling (vendored in-repo, not an external dependency) | | core/mcpplug.ts | Gateway tool sourcing (e.g. Composio) (vendored in-repo, not an external dependency) |

Installing loopengine pulls in actauth as a regular dependency — install it directly only if you want to use it standalone. Everything else in this table ships inside loopengine itself.

Deployment

docker compose up --build

Builds the HTTP adapter into a container and starts it alongside Redis. For production, push the image to any container platform with REDIS_URL and your model/tool API keys set as environment variables.

Hosting behind a platform

These settings are for running LoopEngine inside a hosting platform (one workspace per user, behind a router that has already authenticated them). All are off by default; a self-hosted install that doesn't set them behaves exactly as described above.

| Setting | What it does | | --- | --- | | LOOPENGINE_TRUSTED_PROXY_SECRET | Every request must carry an X-LoopEngine-Identity header signed with this secret (signProxyIdentity creates one; unsigned requests get 401). LOOPENGINE_ADMIN_AUTH, if also set, still works for direct access. Also enables GET /internal/activity: in-flight turns, open streams and idle time, so the host never stops a server mid-turn. | | LOOPENGINE_EXIT_ON_CODE_CHANGE=1 | After an ability install, upgrade, uninstall or dependency install through the Admin UI, the server drains and exits so the platform restarts it with the new code loaded. Only set this when something restarts the process. | | LOOPENGINE_USAGE_LOG | stdout (one JSON line per model call) or a file path (JSON lines) — tokens per call with agent, tenant, session and model. Or call setUsageSink() in code. | | setSecretStore() | In code, at startup: keep env values set through the Admin UI somewhere other than .env files (a vault, a secret manager). The default FileSecretStore is today's behaviour. |

GET /healthz answers {"ok":true} with no login, on every server, for health checks; it returns 503 once the server starts shutting down, and doesn't count as activity.

deploy/workspace/Dockerfile is a runtime image for this: it serves a scaffolded project mounted at /workspace instead of baking agents in.

License

LoopEngine is open source under the MIT License: use, change, self-host and redistribute it for anything, commercial use included.

Third-party software included in LoopEngine keeps its own license: see THIRD_PARTY_NOTICES.md.