@band-ai/band-sdk-core

中文文档
6.2.0发布于 2026/11/06SEE LICENSE IN LICENSETypeScriptCommonJS零依赖近 30 天下载 2416

Shared Band event-payload validation

原始使用文档
文档目录17 节
  1. Install
  2. Quickstart
  3. Public surface
  4. validateEventPayload(eventType, raw, traceContext?)
  5. Delivery-state runtime classes
  6. SubscriptionTracker lifecycle
  7. DecisionRegistry lifecycle
  8. Session — WebSocket reconnect state machine
  9. Session lifecycle
  10. validateMemoryTypeForSystem(system, type, traceContext?)
  11. Enumerating memory-taxonomy values
  12. One-shot delivery lifecycle
  13. Turn outcome
  14. Build / test
  15. Room-activity observations
  16. License
  17. Fenced delivery settlement

@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");

版本与发布记录

@band-ai/band-sdk-core 共发布 38 个版本,首次发布于 2026/08/21;过去 12 个月发布了 38 个版本。

版本发布日期距今
6.2.0latest2026/11/062 天前
6.1.22026/11/025 天前
6.1.12026/10/2612 天前
6.1.02026/10/2613 天前
6.0.02026/10/2513 天前
5.0.02026/10/1919 天前

其他发布通道:dev → 0.4.0-dev.2。

关于 @band-ai/band-sdk-core 的常见问题

@band-ai/band-sdk-core 是什么?

Shared Band event-payload validation

如何安装 @band-ai/band-sdk-core?

在项目目录运行 npm install @band-ai/band-sdk-core,或使用 pnpm add / yarn add / bun add。

@band-ai/band-sdk-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@band-ai/band-sdk-core 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@band-ai/band-sdk-core 可以商用吗?

@band-ai/band-sdk-core 使用 SEE LICENSE IN LICENSE 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@band-ai/band-sdk-core 的下载量有多少?

根据 npm 官方下载统计,@band-ai/band-sdk-core 在 2026/10/08 – 2026/11/06 这 30 天内被下载 2,416 次,日均约 81 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@band-ai/band-sdk-core 还在维护吗?

最新版本 6.2.0 发布于 2026/11/06(2 天前);至今共发布 38 个版本,其中过去 12 个月发布了 38 个;首次发布于 2026/08/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。