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

@not-another-harness/nah-js

v0.0.1-beta.9

Published

The standalone name for not-another-harness/js: a Web runtime for building agents in your own application. Mount the harness on any Request -> Response handler, stream its events over SSE, and let a paused turn resume on the next request.

Readme

nah-js

A Web runtime for not-another-harness — the standalone name for not-another-harness/js. Mount the harness on any handler that takes a Request and returns a Response, stream its events to the browser over server-sent events, and let a paused turn resume on the next request.

Install

npm install nah-js not-another-harness ai zod

[email protected] is not on npm yet. Until it is, install the runtime from the SDK package and import it as not-another-harness/js — the same code, the same exports:

npm install not-another-harness ai zod

The runtime is implemented in the SDK package, at not-another-harness/js; this package re-exports it and holds no code of its own. Two names, one implementation — depend on nah-js if you would rather not import a subpath of the SDK, or on not-another-harness/js if you would rather keep a single dependency. The exports are identical either way.

nah-js is the transport: it decides how a run crosses the network. The agent — the model, the tools, the system prompt, the budget — stays yours, because that is the part an application should own.

There is no framework dependency. A route handler is a function that takes a Request and returns a Response, and that is the whole contract this package speaks.

Mount it

// app/api/nah/route.ts — Next.js
import { createNahHandler } from "nah-js";
import { anthropic } from "@ai-sdk/anthropic";

export const runtime = "nodejs";
export const maxDuration = 300;

export const { GET, POST } = createNahHandler({
  agent: ({ sessionId }) => ({
    model: anthropic("claude-sonnet-4-5"),
    system: "You are a helpful assistant.",
    tools: {},
  }),
});

Two handlers come back:

| Route | Method | What it does | | --- | --- | --- | | /api/nah | POST | Runs a turn, or steers or stops a run that is in flight. The response is an event stream. | | /api/nah?sessionId=…&after=… | GET | Replays the frames a client missed, then follows along live. |

The agent factory is called once per turn, after the transcript has been loaded, so the model, the tools, or the system prompt can vary per conversation — and so the tools can be built against the workspace that conversation is about.

Read it in the browser

"use client";
import { useNahChat } from "nah-js/client";

export function Chat() {
  const chat = useNahChat();

  return (
    <div>
      {chat.messages.map((message) => (
        <p key={message.id}>
          <strong>{message.role}:</strong> {message.text}
        </p>
      ))}

      {chat.pendingApprovals.map((approval) => (
        <button
          key={approval.approvalId}
          onClick={() => chat.approve(approval.approvalId, true)}
        >
          Run {approval.toolName}
        </button>
      ))}

      <button onClick={() => chat.send("hello")}>Send</button>
    </div>
  );
}

nah-js/client is a separate entry point, so React never reaches a server bundle. The hook keeps the transcript, the pending decisions, and the status; it does not decide what your UI looks like.

The one thing that shapes the protocol

A turn can stop and be continued by a later request.

The harness ends a run with awaiting-approval when a tool needs a person's decision, and with suspended when a tool parked itself waiting for a value. Neither is an error: the run is paused. The transcript holds a call that never produced a result, and the only way forward is to run the harness again with the transcript and the answer.

So the transcript lives in a store rather than in the request, finish carries the pending decisions, and a resume is a fresh POST carrying the decision — not a second message on the same connection.

A consequence worth knowing: a decision and a new message cannot share a request. The harness reads a decision from the last message of the transcript only, so a request carrying both would push the decision out of the position the SDK reads — the approval would be silently ignored, the tool would never run, and the run would report success having done nothing. send and approve are separate calls for that reason, and the runtime refuses a request that tries to do both.

The wire format

Every frame is a JSON object with a type, a seq, the sessionId and the runId:

id: 12
event: text-delta
data: {"type":"text-delta","seq":12,"sessionId":"…","runId":"…","at":1730000000000,"step":1,"text":"Hello"}

id carries the sequence number, so a client that reconnects can ask for everything after frame 42 and get it. event carries the type, so a browser's EventSource can dispatch by name. An event this runtime does not know about is skipped rather than crashing the connection.

The frames a client can receive are turn-start, run-start, step-start, text-delta, tool-call, tool-result, tool-approval-request, tool-approval-response, tool-suspended, tool-resumed, step-finish, compacted, wrap-up, user-message, finish, error and ping. They are typed in src/types.ts and exported from the package root.

Where the state lives

Two seams, both yours to fill:

  • session — where transcripts live between requests. load, append and replace; replace exists because compaction means the transcript was rewritten, and a store that cannot represent that has to take the whole thing or lose history.
  • events — where frames are recorded, so a reconnect can be answered. Separate from the session store because the two have different lifetimes: the transcript is the conversation, the log is a record of the delivery of its events.

Both default to in-memory implementations, which are right for a single long-lived Node server and wrong for anything else. On a serverless platform each request may land on a different instance, and a second instance sees an empty conversation. Implement the two interfaces against the store you already have.

Where it runs

The core speaks the Web platform's request and response objects, so the same runtime mounts in:

  • Next.js — a route handler, as above.
  • Hono, Remix, SvelteKit, Astro — an endpoint that receives a Request and returns a Response.
  • Cloudflare Workers, Deno, Bun, Node — the same, with the store you have.

What a framework adds is its own plumbing, and that plumbing stays in the application:

// Next.js: work that should outlive the response.
import { after } from "next/server";

export const { GET, POST } = createNahHandler({
  agent: () => ({ model, system, tools }),
  after,
});

after is optional. The runtime uses it for the bookkeeping it does after a turn ends, and falls back to running that bookkeeping inline when the handover is not available — a Next.js after throws when it is called outside the request scope, and a turn can end after the response was written.

Notes for a real deployment

  • Write the transcript before the response finishes. A client that sends its next turn the moment it sees finish must find the transcript already there. The runtime does this; a custom session store should too.
  • The request's own AbortSignal is the run's abort signal. A client that closes the tab stops the run rather than leaving it burning tokens for an answer nobody will read.
  • The follow stream ends when the turn it is following is over. A client that wants the next turn reconnects, which a browser's EventSource does on its own.
  • Errors are reduced to a name and a truncated message before they reach the wire — a stack trace or a provider message with a key in it does not belong in a browser. The original stays on the server, where the logs are.

Development

pnpm --filter nah-js test
pnpm --filter nah-js lint
pnpm --filter nah-js build

License

MIT