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

@looppause/langgraph

v0.1.1

Published

Native LoopPause human-in-the-loop nodes for LangGraph (TypeScript)

Readme

@looppause/langgraph

Native LoopPause human-in-the-loop nodes for LangGraph (TypeScript).

"The missing primitive so agents don't go rogue — or die waiting for approval."


What it does

Provides two factory functions that wrap a LoopPause approval checkpoint into a LangGraph node, plus a router and a webhook-resume helper:

| Export | Pattern | Use when | |---|---|---| | makePollingGate | Long-poll until responded | Simple flows, short timeouts, no checkpointer needed | | makeInterruptGate | Creates pause → interrupt() → resumes via webhook | Long timeouts, durable graphs, multi-turn agents | | routeOnGate | Conditional edge helper | Both patterns | | resumeThread | Wake a hibernating graph | makeInterruptGate only |

Both patterns fail closed: if LoopPause is unreachable or the response is rejected, approved is false and the graph routes to "halt". system_fallback is never treated as human approval.


Installation

npm install @looppause/langgraph @langchain/langgraph

Requires Node.js 18+ (uses global fetch and node:crypto).


Configuration

| Environment variable | Required | Description | |---|---|---| | LOOPPAUSE_API_KEY | ✅ Yes | Your API key (sk_live_…) | | LOOPPAUSE_SIGNING_SECRET | ✅ Yes | Signing secret for HMAC verification | | LOOPPAUSE_API_URL | No | Override API base URL (default: https://api.looppause.com) |

Get your keys at looppause.com/dashboard.


Pattern 1: Polling gate

The node creates a pause and holds the graph open, polling until the human responds or the deadline passes. Simple — no checkpointer, no webhook.

import { StateGraph, Annotation } from "@langchain/langgraph";
import {
  LoopPauseClient,
  makePollingGate,
  routeOnGate,
} from "@looppause/langgraph";

const GraphState = Annotation.Root({
  amount: Annotation<number>(),
  vendor: Annotation<string>(),
  looppause: Annotation<any>(),
});

const client = new LoopPauseClient(); // reads env vars

const approveTransfer = makePollingGate(client, {
  agentId: "billing-agent",
  actionBuilder: (state) => ({
    type: "approval",
    description: `Transfer £${state.amount} to ${state.vendor}`,
    details: { amount: String(state.amount), vendor: state.vendor },
  }),
  recipients: [
    { channel: "slack", target: "#finance-approvals" },
  ],
  timeoutHours: 4,
});

const builder = new StateGraph(GraphState)
  .addNode("approve_transfer", approveTransfer)
  .addNode("execute_transfer", executeTransfer)
  .addNode("halt", haltNode)
  .addEdge("__start__", "approve_transfer")
  .addConditionalEdges("approve_transfer", routeOnGate(), {
    approved: "execute_transfer",
    halt: "halt",
  });

const graph = builder.compile();

const result = await graph.invoke({ amount: 12450, vendor: "Globex Corp" });
// result.looppause.approved === true  → transfer executed
// result.looppause.approved === false → graph halted, reason in haltReason

Reading the gate result:

interface GateResult {
  approved: boolean;          // true only when human authorized AND decision === "approved"
  authorizationType: string;  // "human" or "system_fallback"
  responder: string;          // email of the human who responded
  comment: string | null;     // optional comment from the approver
  pauseId: string;            // LoopPause pause_id for audit
  haltReason: string | null;  // set when approved === false
}

Pattern 2: Interrupt gate (durable, webhook-driven)

The interrupt gate creates a pause, then calls LangGraph's interrupt() to hibernate the graph in the checkpointer. When LoopPause fires the webhook, your handler wakes the graph with the signed proof via resumeThread.

This is the right pattern for long-running approvals (hours, days) where you cannot keep a connection open.

The reentrancy fix

LangGraph re-executes the node from the top on resume, so createPause is called twice for the same action. makeInterruptGate passes a deterministic Idempotency-Key derived from (thread_id, action) — the second call returns the existing pause_id rather than creating a duplicate.

Full example

import { StateGraph, Annotation, MemorySaver } from "@langchain/langgraph";
import {
  LoopPauseClient,
  makeInterruptGate,
  routeOnGate,
  resumeThread,
  type Proof,
} from "@looppause/langgraph";

const GraphState = Annotation.Root({
  amount: Annotation<number>(),
  vendor: Annotation<string>(),
  looppause: Annotation<any>(),
});

const client = new LoopPauseClient();
const checkpointer = new MemorySaver(); // use LangGraph Cloud or Redis in prod

const approveTransfer = makeInterruptGate(client, {
  agentId: "billing-agent",
  webhookUrl: "https://api.yourapp.com/webhooks/looppause",
  actionBuilder: (state) => ({
    type: "approval",
    description: `Transfer £${state.amount} to ${state.vendor}`,
    details: { amount: String(state.amount), vendor: state.vendor },
  }),
  recipients: [
    { channel: "slack", target: "#finance-approvals", fallback_email: "[email protected]" },
  ],
  timeoutHours: 24,
});

const builder = new StateGraph(GraphState)
  .addNode("approve_transfer", approveTransfer)
  .addNode("execute_transfer", executeTransfer)
  .addNode("halt", haltNode)
  .addEdge("__start__", "approve_transfer")
  .addConditionalEdges("approve_transfer", routeOnGate(), {
    approved: "execute_transfer",
    halt: "halt",
  });

const graph = builder.compile({ checkpointer });

// Start the graph — it hibernates at interrupt()
const threadId = "thread_abc123";
await graph.invoke(
  { amount: 12450, vendor: "Globex Corp" },
  { configurable: { thread_id: threadId } },
);

// Later — your webhook handler receives the signed proof from LoopPause:
async function handleLoopPauseWebhook(req: Request): Promise<void> {
  const proof = (await req.json()) as Proof;
  // threadId must be recoverable from the proof — store it when creating the pause,
  // or encode it in the webhook URL: /webhooks/looppause?thread=thread_abc123
  await resumeThread(graph, threadId, proof);
}

Storing the thread ID

makeInterruptGate does not automatically map pause_idthread_id. A minimal pattern: store the mapping when the graph starts, keyed on the pause_id returned from the interrupt() value:

// After graph.invoke() returns (or throws NodeInterrupt), the pause_id is in
// the interrupted node's value. A simpler approach: encode thread_id in the
// webhook URL path or query param.

// Webhook URL per thread:
webhookUrl: `https://api.yourapp.com/webhooks/looppause/${threadId}`,

Escalation

Pass an escalation config to automatically escalate if the primary recipient does not respond:

makePollingGate(client, {
  // ...
  escalation: {
    after_hours: 2,
    recipients: [{ channel: "email", target: "[email protected]" }],
  },
});

API reference

LoopPauseClient

new LoopPauseClient(opts?: {
  apiKey?: string;           // default: LOOPPAUSE_API_KEY env var
  signingSecret?: string;    // default: LOOPPAUSE_SIGNING_SECRET env var
  baseUrl?: string;          // default: https://api.looppause.com
  requestTimeoutMs?: number; // default: 10 000
})

makePollingGate(client, config)

Returns a LangGraph node function. Polls until responded or deadline.

interface GateConfig {
  agentId: string;
  actionBuilder: (state: any) => Record<string, unknown>;
  recipients: Array<Record<string, unknown>>;
  stateKey?: string;         // default: "looppause"
  requireHuman?: boolean;    // default: true — system_fallback is never approved
  timeoutHours?: number;     // default: 24
  escalation?: Record<string, unknown>;
}

makeInterruptGate(client, config)

Returns a LangGraph node function. Creates pause → interrupt() → resumes with signed proof. webhookUrl is required.

routeOnGate(stateKey?)

Returns a conditional edge function: "approved" when state[stateKey].approved is true, otherwise "halt".

resumeThread(graph, threadId, signedProof)

Wakes a hibernating graph thread with the signed proof as the interrupt resume value. Call from your webhook handler.

verifySignature(proof, signingSecret)

Verifies the HMAC-SHA256 signature on a proof object. Timing-safe.


Relationship to @looppause/mcp

| | @looppause/langgraph | @looppause/mcp | |---|---|---| | For | LangGraph TypeScript graphs | Any MCP-compatible agent (Claude Code, Cursor) | | Pattern | Graph node + conditional edge | Two MCP tools: request_approval + check_approval | | Reentrancy | Handled via idempotency key | Not applicable | | Verification | HMAC-SHA256 via verifySignature | Ed25519 via verifyLoopPauseProof or verify_proof tool |


Local development

# From the monorepo root
cd packages/langgraph
npm run build       # tsc → dist/
npm run type-check  # no emit

License

MIT