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

@rarebit-one/harness-kernel

v0.6.0

Published

Provider-neutral AI-harness kernel — LLM adapters, tool-use loop, MCP connectors, and a pluggable agent-engine seam

Readme

harness-kernel

A provider-neutral AI-harness kernel in TypeScript: LLM adapters, a tool-use loop, MCP connectors, sandbox primitives, and a pluggable agent-engine seam.

It is deliberately a kernel, not a framework. It holds the commodity mechanics every AI harness needs and nothing else — applications are the userland. The kernel never knows an application exists; dependencies point one way only (app layer → kernel, never the reverse).

                    harness-kernel
             ▲            ▲            ▲
      app harness   app harness   app harness      (one-way deps)

Status

v0 — public, published. @rarebit-one/harness-kernel is on npm, MIT licensed. Install it the ordinary way:

npm install @rarebit-one/harness-kernel

Releases publish from publish.yml via npmjs OIDC trusted publishing — no token exists anywhere in this repo. The workflow ships inert: the mutating step is skipped unless the repo variable PUBLISH_LIVE is "true", so a Release cut by accident is a loud no-op. Every version from 0.5.2 on carries a build provenance attestation.

v0 is a mechanical extraction of a proven, already-provider-neutral agent core into a standalone package — the mechanics are battle-tested, the packaging is new. Seam-widening (a general ModelInvocation seam, streaming, multimodal input, middleware, context providers) is planned as an additive v0.2.

Install

npm install @rarebit-one/harness-kernel   # once published

ESM-only, Node >= 22.

What's in it

| Area | Exports | |------|---------| | Model seam | ModelInvocation — one arrow for every model kind, with declared capabilities, a uniform result envelope and probe() | | Providers | Provider interface + selectProvider() over Anthropic, OpenAI, OpenRouter, a self-hosted local (any OpenAI-compatible server), and an offline mock; chatModel() puts any of them on the seam | | Agent loop | runAgent() — a provider-neutral tool-use loop with step and wall-clock budgets | | Loop | Loop + nativeLoop — the control loop as a seam; the kernel ships one and it is the loop runAgent has always run | | Run events | RunEvent — an ordered, structured account of a run (turns, tool calls, budgets, outcome), emitted to an optional sink | | Tools | Tool, primitiveTools() (run_code, read_file, list_files, http_fetch), connectorTools(), modelAsTool(), metadata + projections | | Connectors | connectMcp() — MCP clients over stdio / streamable HTTP / SSE | | Engines | The AgentEngine seam + native, claude-code, and codex harnesses, plus the capability mechanism (scope guards, a generic write_file, and MCP/stdio transports) — the capability set is yours to inject | | Routing | RouteResolver + StaticRouteResolver — capability → model, prompt, tools | | Context | ContextProvider chain, assembled in parallel and rendered into the prompt | | Secrets | secretsToEnv() — resolved secret values into an exec environment |

Extension points

The kernel is meant to be extended from outside, never patched from inside. There are seven seams, and each ships with the in-process default that makes it usable on its own — no database, no service, no infrastructure.

1. Model kinds. A chat LLM is one kind among several. Register any other and resolve it back through the same call:

import { ModelRegistry, chatModel, selectProvider } from "@rarebit-one/harness-kernel"

const registry = new ModelRegistry()
  .registerInstance(chatModel(selectProvider("anthropic")))
  .register({ kind: "vision.detect", id: "yolo" }, () => myDetector)

const detector = registry.resolve<DetectRequest, DetectionResult>({
  kind: "vision.detect",
  id: "yolo",
})

Req and Res stay typed per kind — the seam unifies the arrow and the envelope (ModelResult, InvokeContext, ModelCaps, probe), never the payload. An unresolved ref throws rather than returning undefined.

2. Route resolution. One layer up: which model, prompt and tools serve a named capability. StaticRouteResolver is capabilities-as-code; a dynamic resolver is your adapter behind the same interface.

const resolver = new StaticRouteResolver([
  { name: "summarise", model: { kind: "chat", id: "anthropic" }, prompt: "Summarise." },
])
const route = await resolver.resolve("summarise")
const model = registry.resolve(route.model) // the resolver picks, the registry binds

3. Middleware. Payload-blind, so one implementation covers every kind:

const model = withMiddleware(anyModel, [correlationMiddleware(), loggingMiddleware()])

4. Engines. The AgentEngine seam — native, claude-code, codex, or your own. File mutations stay out of band.

5. Context providers. Replace a single pre-baked context string with a chain, so a live source can inject fragments:

new NativeEngine({ contextProviders: [myLiveFeed] })

Providers run in parallel; one that fails is logged and skipped rather than failing the run.

6. Tools. Rich metadata (capability scoping, invocation-mode gating, confirmation, reversibility, retention, client visibility) with lossy projections down to what a provider actually needs — and modelAsTool(), which surfaces any model invocation as a callable tool. That last one is the cleanest form of extension: a perception model becomes something the chat loop can call without the loop, the engines, or any kernel code learning perception exists.

7. The loop. The control flow itself. Everything above answers "which implementation?" for something the loop uses; this is the loop.

const confirmFirst: Loop = {
  name: "confirm-first",
  run: async (req, ctx) => {
    const gated = req.tools.filter((t) => t.meta?.requiresConfirmation)
    if (gated.length > 0) return { text: "awaiting confirmation", steps: 0, outcome: "completed" }
    return nativeLoop.run(req, ctx)
  },
}
new NativeEngine({ loop: confirmFirst })

A loop is handed resolved materials — a bound model, built prompts, a projected tool surface, budgets as numbers rather than optionals (via resolveLoopLimits, the one place defaults live). It decides control flow and nothing else, which is what keeps a second implementation small enough to be worth writing. runAgent and nativeLoop are the same code path, so they cannot drift.

Capabilities are checked, not assumed: asking a model to stream when it declared streaming: false throws a CapabilityError instead of quietly returning a buffered response.

Run events

log is prose for a human. The event stream is the same run, machine-readable:

import { runAgent, recordRunEvents } from "@rarebit-one/harness-kernel"

const { sink, events } = recordRunEvents()
await runAgent({ provider, system, userPrompt, tools, emit: sink })
// events: run.started -> model.turn -> tool.called -> tool.succeeded -> ... -> run.finished

The bookendsrun.started and run.finished — are emitted by runWithEvents, which wraps whichever Loop runs. A loop emits only what happens inside it, so a custom loop that emits nothing still produces a run that is visibly a run. A guarantee that depends on every future implementor remembering it is not a guarantee.

On the failure path steps and text are omitted, not zeroed: the loop threw and never returned a result, so any number there would be invented. The model.turn events already in the stream are the authoritative record of how far it got.

Engines take the same sink through EngineContext:

await engine.run(spec, { log, emit: sink })

Both are optional — omit them and the loop behaves exactly as it did before events existed. Every event carries a monotonic seq from a single per-run counter, so a gap is detectable: a dropped event must never look like an event that never happened. A sink that throws is reported through log and skipped, never allowed to fail the run.

A sink is an observer and can never be a participant: tool.called.input is a detached copy, so a sink that redacts in place cannot change what the tool receives. The flip side is a disclosure surface worth knowing about — that field carries the raw tool arguments, so any secret or PII a model passed as an argument now reaches every attached sink. Nothing outside the model conversation captured these before; a sink that persists or forwards events should redact.

run.finished is always the last event, including when the loop throws — the error propagates unchanged, but outcome: "failed" closes the stream first, so a crashed run is never mistaken for one still in flight.

The kernel emits this stream; it does not store it. Resume, fork, search and replay all want persistence and a schema — infrastructure a kernel must not ship. recordRunEvents() is an in-memory recorder for tests and the reference shape for a real store, not a session store itself.

tool.succeeded carries the tool's own reversible / undoToolName / undoWindowSeconds from ToolMetadata, copied at call time. That is the missing half of a bargain the metadata already made: it could declare a tool undoable but nothing recorded that the tool ran, so an application had nothing concrete to undo. Deciding whether to undo remains policy, and stays in the application.

Usage

import { selectProvider, runAgent, primitiveTools } from "@rarebit-one/harness-kernel"

const provider = selectProvider("anthropic", { model: "claude-sonnet-5" })

const text = await runAgent({
  provider,
  system: "You are a helpful autonomous agent.",
  userPrompt: "Summarise the repo's README.",
  tools: primitiveTools("/path/to/sandbox", process.env),
})

With no provider API key configured, selectProvider falls back to the offline mock provider — so the whole loop runs end to end with no keys and no network. That is deliberate: the test suite depends on it, and so can yours.

Self-hosted (local). selectProvider("local") runs against any OpenAI-compatible server — llama-server, vLLM, LM Studio, Ollama's OpenAI endpoint — set LOCAL_MODEL_BASE_URL (required), and optionally LOCAL_MODEL and LOCAL_MODEL_API_KEY (a local server usually needs none; when absent no Authorization header is sent). Unlike the others, local fails loud when no base URL is configured rather than degrading to the mock — it is chosen to keep a run on private inference (e.g. jumpdrive-web's data_class: pii, which forces a run onto the org's designated model), so silently running it elsewhere would defeat the point.

Engines

An engine is a pluggable agent harness. native is the in-process tool-use loop above; claude-code and codex shell out to those CLIs/SDKs. All three share one capability surface (open_issue, write_file, promote_knowledge) scoped to a single run's workspace, and all three mutate files out of band — the caller diffs the working tree afterwards, so the engine contract never has to model "an edit" uniformly.

import { selectEngine } from "@rarebit-one/harness-kernel"

const engine = selectEngine("native", { domainTools }) // "claude-code" | "codex" | unknown → native
const support = engine.supports(spec)
if (!support.ok) throw new Error(support.reason) // fail loud, never silently degrade
const { text } = await engine.run(spec, { log: console.error })

The kernel defines no domain tools

primitiveTools() is strictly generic — sandboxed shell/file/HTTP access. Tools that collect application output don't exist here at all: what a run may emit is your vocabulary, so you define it and inject it.

import { primitiveTools, writeFileCapability, denyCrossWorkspace } from "@rarebit-one/harness-kernel"

// Your sinks, your tools — the kernel never sees them.
const knowledge: MyNote[] = []
const domainTools = (allowed?: string[]) => myEmissionTools({ knowledge }, allowed)

const engine = selectEngine("native", { domainTools })

The same holds for the workspace-scoped capabilities the external engines mount over MCP. The kernel ships exactly one — writeFileCapability, a sandbox write with no product semantics — plus the guards (denyCrossWorkspace, resolveWithin) so your own capabilities inherit identical security properties:

selectEngine("claude-code", {
  capabilityTools: (ctx) => [writeFileCapability(ctx), ...myCapabilities(ctx)],
})

// codex runs out-of-process, so it needs a script it can spawn — a child
// process can't be handed a closure. Omitting it throws rather than serving an
// empty surface.
selectEngine("codex", { capabilityServerScript: "/app/dist/my-capability-server.js" })

Development

npm ci
npm run typecheck      # tsc --noEmit
npm run lint           # eslint .   (lint:fix to autofix)
npm run format         # prettier --write .   (format:check to verify)
npm test               # vitest run
npm run build          # tsc -p tsconfig.build.json → dist/ + .d.ts

License

MIT © Rarebit One