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

@band-ai/band-sdk-core

v6.1.1

Published

Shared Band event-payload validation

Readme

@band-ai/band-sdk-core

Shared Band event-payload validation, memory-taxonomy, and inbound-delivery runtime state, compiled from Rust to WebAssembly for Node. Every function is synchronous, pure computation — no network, filesystem, or logging.

band-sdk-core (Rust)  -->  @band-ai/band-sdk-core (this package, npm)  -->  band-sdk-typescript

If you're using band-sdk-typescript, it already depends on this package — you don't need to install it yourself. Install it directly only if you're calling into it without that SDK. Full picture: repository architecture.

Install

npm install @band-ai/band-sdk-core

The package is Node CommonJS with an eager WASM load — there is no init step, and no Rust or wasm-pack toolchain needed to install. Both require() and ESM import() resolve it at the package root.

Quickstart

const { validateEventPayload } = require("@band-ai/band-sdk-core");

try {
  const payload = validateEventPayload("room_deleted", { id: "room-1" });
} catch (err) {
  for (const { path, code, message } of err.issues) {
    console.error(`${path}: ${code} - ${message}`);
  }
}

validateEventPayload is the platform's inbound WebSocket payload policy: normalize a well-formed payload, or throw with every violation reported at once, never just the first.

Public surface

validateEventPayload(eventType, raw, traceContext?)

eventType is the platform event name ("message_created", "agent.control", …). raw is a plain JS value. Success returns the normalized payload (event_created returns the input unchanged). Failure throws a native Error with .issues ({path, code, message} objects) and .traceContext — not a message-only error. There is no custom exception class.

Delivery-state runtime classes

ClaimRegistry, RetryTracker, ParticipantRoster, and SubscriptionTracker are the inbound-delivery runtime state classes — lifecycle and design decisions live as rustdoc on each type in crates/core/src/runtime/.

A participant is any plain object; add/setAll read id, name, type, handle, description from it and list() returns objects with exactly those five keys. id is required and must be a string; the other four are each a string or null/absent. setAll takes an optional traceContext and throws — leaving the roster unchanged, with .issues and .traceContext attached like validateEventPayload's error — if its snapshot names the same id twice. RetryTracker's maxTracked must be at least 1; 0 throws. ClaimRegistry's maxCompleted must be at least 1 too — 0 also throws.

ClaimRegistry keeps one terminal intent per message until rememberCompleted or matching confirmSettlement: rememberAckPending records a processed write and rememberFailedAckPending a failed one carrying its error. The first intent wins; a different one throws — leaving the entry unchanged, with .issues and .traceContext attached — and an identical repeat is a no-op. pendingAck returns {status, error?, parked} or undefined. parkAck takes a ParkedReason wire name, parkedReasons() lists them, and any other value throws a plain Error.

SubscriptionTracker provides synchronous, transport-independent decisions for agent-topic joins and the two-topic room subscription transaction. Its opaque tickets are JavaScript bigint values and must be supplied when recording a completion. Failed rollbacks and failed or unknown leaves require explicit reconciliation before a fresh claim is allowed.

SubscriptionTracker lifecycle

import { SubscriptionTracker } from "@band-ai/band-sdk-core";

const tracker = new SubscriptionTracker();
const ticket = tracker.beginRoomSubscribe("room-1");
if (ticket === undefined) throw new Error("room is not claimable");

switch (tracker.recordRoomParticipantsJoinFailed("room-1", ticket, false)) {
  case "rollback_failed":
    if (tracker.roomStatus("room-1") !== "needs_reconciliation") {
      throw new Error("rollback state was not retained");
    }
    tracker.acknowledgeRoomReconciled("room-1");
    const retryTicket = tracker.beginRoomSubscribe("room-1");
    if (retryTicket === undefined) throw new Error("reconciled room is not claimable");
    if (tracker.recordBothRoomTopicsJoined("room-1", retryTicket) !== "subscribed") {
      throw new Error("room did not subscribe");
    }
    const leaveTicket = tracker.unsubscribeRoom("room-1");
    if (leaveTicket === undefined || !tracker.markRoomLeaveComplete("room-1", leaveTicket, "left")) {
      throw new Error("room did not leave");
    }
    break;
  case "subscribed":
  case "join_failed":
  case "rolled_back":
  case "stale":
    break;
}

DecisionRegistry lifecycle

DecisionRegistry tracks chat-mediated asks: whoever claims one first owns its outcome. Every registration returns [token, ticket], with ticket a bigint, and later calls name that exact registration, so a stale holder gets "stale" instead of acting on a newer ask that reused the token. maxPending must be an integer of at least 1, or omitted/null for unbounded; seed is an optional bigint. cancelAll/cancelRoom return a plain { unclaimed, claimed } object. isAuthorizedSender takes an array, a Set, or null.

import { DecisionRegistry, isAuthorizedSender } from "@band-ai/band-sdk-core";

const registry = new DecisionRegistry(10);
const registration = registry.registerMinted("room-1");
if (registration === undefined) throw new Error("tickets exhausted");
const [token, ticket] = registration;

if (registry.tryClaim(token, ticket) !== "claimed") throw new Error("not claimed");
if (registry.tryClaim(token, ticket) !== "already_claimed") throw new Error("claimed twice");
if (registry.cancelRoom("room-1").claimed[0] !== token) throw new Error("claim not reported");
if (!isAuthorizedSender(new Set(["alice"]), "alice")) throw new Error("alice refused");

Session — WebSocket reconnect state machine

Session/SessionPolicy are a sans-io session state machine plus reconnect backoff/jitter policy; classifyClose/classifyUpgrade classify a WebSocket close code or HTTP upgrade-rejection status. Session never sleeps, connects, or closes a socket itself — the caller drives its own transport and reports what happened through onConnected/ onSocketClose/onUpgradeRejected/onSupersede. Its epoch tickets are JavaScript bigint values, matching SubscriptionTracker's. Decisions live as rustdoc in crates/core/src/runtime/session.rs.

Session lifecycle

import { Session, SessionPolicy } from "@band-ai/band-sdk-core";

const session = new Session(SessionPolicy.default());
const epoch = session.beginAttempt(0.0);
if (epoch === undefined) throw new Error("fresh session must yield an epoch");

const connected = session.onConnected(epoch, 0.0);
if (connected.state !== "up") throw new Error("expected up");

const disconnected = session.onSocketClose(epoch, 5.0, 1006, 0.5);
if (disconnected.state !== "reconnecting") throw new Error("expected reconnecting");
if (disconnected.retryAfterS === undefined) throw new Error("expected a retry delay");

validateMemoryTypeForSystem(system, type, traceContext?)

Validates the canonical memory taxonomy — design decisions live as rustdoc in crates/core/src/memory.rs. system and type are wire-name strings (MemorySystem/MemoryType in index.d.ts are string-literal types, not classes — there is no runtime enum on this boundary, matching how EventType stays a plain string in validateEventPayload). Returns void on success. Failure throws a native Error with the same .issues/.traceContext contract as validateEventPayload — an unrecognized system and an unrecognized type are independent issues, both reported when both strings are invalid. See "Enumerating memory-taxonomy values" below for listing each enum's members without introducing one.

Enumerating memory-taxonomy values

memorySystems(), memoryTypes(), memorySegments(), memoryStoreScopes(), memoryListScopes(), and memoryStatuses() each return that enum's wire names as a string[], in declaration order — runtime enumeration without introducing a class, matching MemorySegment/MemoryStoreScope/ MemoryListScope/MemoryStatus as string-literal types in index.d.ts.

import { memorySystems, memoryStatuses } from "@band-ai/band-sdk-core";

if (!memorySystems().includes("sensory")) {
  throw new Error("memorySystems() missing sensory");
}
if (!memoryStatuses().includes("active")) {
  throw new Error("memoryStatuses() missing active");
}

One-shot delivery lifecycle

evaluateDeliveryEvent, evaluateNextMessage, evaluateDrainCandidate, and evaluateAdapterResult are the stateless one-shot delivery lifecycle decisions — design decisions live as rustdoc in crates/core/src/runtime/delivery.rs. Each returns a plain tagged object; there is no cross-invocation state, so there is no class here to construct. Decision fields stay snake_case (message-wire vocabulary: room_id, sender_id, sender_type, …), unlike this module's own camelCase function/argument names, so the same shape returns identically from both bindings.

evaluateDeliveryEvent(eventType, roomId, payload, agentId, traceContext?) routes one inbound event: "ignored" for an unrecognized or out-of-scope eventType; "cleanup" for room_removed/room_deleted; "skip_self" or "invocation" for message_created, depending on whether the sender is this agent (sender_type === "Agent" and sender_id === agentId). message_created delegates to validateEventPayload for payload-shape validation, so a malformed payload throws the same Error, and additionally rejects an empty payload.id — a message with no identity cannot be claimed or acknowledged. room_removed/room_deleted need no payload shape and read id straight off the raw payload.

roomId resolves from the caller-supplied roomId first, else the payload's own chat_room_id (message events) or id (room events). An empty string counts as absent at both steps. Unresolvable throws an Error with one issue on path room_id: code missing when the fallback field is absent or empty, wrong_type when it is present but not a string.

evaluateNextMessage(triggeringMessageId, nextMessageId) compares the triggering message against the platform's authoritative "next open message" for a room (already fetched by the caller): "no_pending", "already_processed", or "ready_to_claim".

evaluateDrainCandidate(candidate, seenIds, agentId) classifies one already-fetched drain candidate — candidate is {id, sender_id, sender_type}, or null/undefined when the fetch returned nothing: "no_candidate", "self_echo" (checked before the snapshot, so an echo never halts a drain), "out_of_snapshot" (stop), or "drain" (continue). The caller's own bounded or unbounded loop owns the cap; this function classifies one candidate per call.

evaluateAdapterResult(roomId, messageId, succeeded) maps an adapter outcome to "processed" or "failed".

isSelfEcho(senderId, senderType, agentId) is the one definition of "self echo" (senderType === "Agent" && senderId === agentId) that evaluateDeliveryEvent and evaluateDrainCandidate use internally, exposed for a host's own call sites that classify a sender without going through either function.

Turn outcome

TurnLedger judges one agent turn — design decisions live as rustdoc in crates/core/src/turn.rs. Create one per turn, record the TurnEffect of each successful tool call (a failed call records nothing), and ask for the verdict() when the model stops: "complete" once the turn replied, declined, or did real work, else "missing_reply" (fail the turn with missingReplyMessage()). settle() marks a turn the SDK answered itself and noteReported() one whose failure was already reported; both complete it. replySettled() is true once a recorded call posted to the room or declined to, so no fallback text is due. record throws a plain Error for anything but a TurnEffect wire name.

bandToolEffects() maps every canonical Band tool name to its TurnEffect; normalize an alias to its canonical name before the lookup. noReplyTool() returns band_no_reply's definition as a ToolSpec (name, description, parameters). turnEffects() and turnVerdicts() list the wire names.

import { TurnLedger, bandToolEffects } from "@band-ai/band-sdk-core";

const ledger = new TurnLedger();
ledger.record(bandToolEffects().band_send_event);
if (ledger.verdict() !== "missing_reply") {
  throw new Error("an observe-only turn is a missing reply");
}

ledger.record("reply");
if (ledger.verdict() !== "complete" || !ledger.replySettled()) {
  throw new Error("a reply must complete the turn");
}

The public declaration is index.d.ts.

Build / test

just check compiles and lints this crate for the host.

just test-wasm   # suite in Node
just pack-npm    # Node tarball + require/import consumers
just build-wasm  # Node package output

Room-activity observations

normalizeRoomActivitySnapshot(raw, selfAgentId, traceContext?) accepts the decoded data object containing working_agents and returns typed native RoomActivityPeer objects. It validates all rows, keeps the last duplicate agent/execution key, excludes self and sorts by agent/execution with null first. Output omits execution IDs. Missing detail and unknown string kinds become reasoning; malformed fields throw Error with issues and traceContext. The canonical field rules live on the Rust normalize_room_activity_snapshot.

import { normalizeRoomActivitySnapshot } from "@band-ai/band-sdk-core";

const peers = normalizeRoomActivitySnapshot({working_agents: [
  {agent_id: "peer", working: true, activity: {kind: "tool_call", name: "search"}},
]}, "self");
if (peers[0].activity.kind !== "tool_call" || peers[0].activity.name !== "search") {
  throw new Error("expected peer tool declaration");
}

Input must be JSON-native: plain objects from any realm (including null-prototype objects) with data properties, dense arrays, finite JavaScript Number values and Unicode scalar strings. Undefined values, sparse arrays, accessors, symbols, nonfinite numbers and lone UTF-16 surrogates throw plain Error before lossy conversion. Maximum depth is 64 (root zero), with 65,536 values and 1 MiB of UTF-8 string content including keys and string arguments. Extra fields are checked too. Cycles and excessive input reject safely. Each call is independent; the SDK owns I/O and availability. This observation does not provide a lock or proof that no peer is working.

License

Proprietary, binary-only Band SDK License Agreement — see the bundled LICENSE. The Rust implementation source is private.

Fenced delivery settlement

guardedClaim shares eligibility across discovery paths. Its optional facts object accepts boolean self_echo, durably_processed, durably_failed, and allow_failed_retry fields. Only allow_failed_retry defaults to true. Unknown fields and non-boolean values throw before ownership changes.

Pending originals remain excluded when parked. Record the terminal disposition before transport and confirm only after acceptance and a current-execution check. Retry confirmation restores charged attempts with the stricter retry ceiling; it does not falsely complete the original. Tokens are opaque local strings.

import { ClaimRegistry, RetryTracker } from "@band-ai/band-sdk-core";

const claims = new ClaimRegistry();
const previous = new RetryTracker(3);
if (claims.guardedClaim("room", "message", previous) !== "runnable") throw new Error("not admitted");
previous.recordAttempt("message");
const pending = claims.recordFailedSettlement("room", "message", "failure", previous, true);
claims.release("room", "message");
const current = new RetryTracker(3);
if (!claims.confirmSettlement("room", "message", pending.token, current)) throw new Error("not confirmed");
if (claims.isCompleted("room", "message")) throw new Error("retry falsely completed");
if (current.recordAttempt("message")[0] !== 2) throw new Error("charge lost");