@band-ai/band-sdk-core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @band-ai/band-sdk-core
2
+
3
+ Shared Band event-payload validation, memory-taxonomy, and inbound-delivery
4
+ runtime state, compiled from Rust to WebAssembly for Node. Every function
5
+ is synchronous, pure computation — no network, filesystem, or logging.
6
+
7
+ ```
8
+ band-sdk-core-core (Rust) --> @band-ai/band-sdk-core (this package, npm) --> band-sdk-typescript
9
+ ```
10
+
11
+ If you're using [`band-sdk-typescript`](https://github.com/band-ai/band-sdk-typescript),
12
+ it already depends on this package — you don't need to install it
13
+ yourself. Install it directly only if you're calling into it without that
14
+ SDK. Full picture: [repository architecture](https://github.com/band-ai/band-sdk-core#architecture).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @band-ai/band-sdk-core
20
+ ```
21
+
22
+ The package is Node CommonJS with an eager WASM load — there is no `init`
23
+ step, and no Rust or `wasm-pack` toolchain needed to install. Both
24
+ `require()` and ESM `import()` resolve it at the package root.
25
+
26
+ ## Quickstart
27
+
28
+ ```js
29
+ const { validateEventPayload } = require("@band-ai/band-sdk-core");
30
+
31
+ try {
32
+ const payload = validateEventPayload("room_deleted", { id: "room-1" });
33
+ } catch (err) {
34
+ for (const { path, code, message } of err.issues) {
35
+ console.error(`${path}: ${code} - ${message}`);
36
+ }
37
+ }
38
+ ```
39
+
40
+ `validateEventPayload` is the platform's inbound WebSocket payload policy:
41
+ normalize a well-formed payload, or throw with every violation reported at
42
+ once, never just the first.
43
+
44
+ ## Public surface
45
+
46
+ ### `validateEventPayload(eventType, raw, traceContext?)`
47
+
48
+ `eventType` is the platform event name (`"message_created"`,
49
+ `"agent.control"`, …). `raw` is a plain JS value. Success returns the
50
+ normalized payload (`event_created` returns the input unchanged). Failure
51
+ throws a native `Error` with `.issues` (`{path, code, message}` objects)
52
+ and `.traceContext` — not a message-only error. There is no custom
53
+ exception class.
54
+
55
+ ### Delivery-state runtime classes
56
+
57
+ `ClaimRegistry`, `RetryTracker`, `ParticipantRoster`, and `SubscriptionTracker` are the
58
+ inbound-delivery runtime state classes — lifecycle and design decisions:
59
+ [`runtime-state-policy.md`](https://github.com/band-ai/band-sdk-core/blob/main/crates/core/docs/runtime-state-policy.md).
60
+
61
+ A participant is any plain object; `add`/`setAll` read `id`, `name`,
62
+ `type`, `handle`, `description` from it and `list()` returns objects with
63
+ exactly those five keys. `id` is required and must be a string; the other
64
+ four are each a string or `null`/absent. `setAll` takes an optional
65
+ `traceContext` and throws — leaving the roster unchanged, with `.issues`
66
+ and `.traceContext` attached like `validateEventPayload`'s error — if its
67
+ snapshot names the same `id` twice. `RetryTracker`'s `maxTracked` must be
68
+ at least `1`; `0` throws. `ClaimRegistry`'s `maxCompleted` must be at
69
+ least `1` too — `0` also throws.
70
+
71
+ `SubscriptionTracker` provides synchronous, transport-independent decisions for
72
+ agent-topic joins and the two-topic room subscription transaction. Its opaque
73
+ tickets are JavaScript `bigint` values and must be supplied when recording a
74
+ completion. Failed rollbacks and failed or unknown leaves require explicit
75
+ reconciliation before a fresh claim is allowed.
76
+
77
+ ### `SubscriptionTracker` lifecycle
78
+
79
+ ```ts
80
+ import { SubscriptionTracker } from "@band-ai/band-sdk-core";
81
+
82
+ const tracker = new SubscriptionTracker();
83
+ const ticket = tracker.beginRoomSubscribe("room-1");
84
+ if (ticket === undefined) throw new Error("room is not claimable");
85
+
86
+ switch (tracker.recordRoomParticipantsJoinFailed("room-1", ticket, false)) {
87
+ case "rollback_failed":
88
+ if (tracker.roomStatus("room-1") !== "needs_reconciliation") {
89
+ throw new Error("rollback state was not retained");
90
+ }
91
+ tracker.acknowledgeRoomReconciled("room-1");
92
+ const retryTicket = tracker.beginRoomSubscribe("room-1");
93
+ if (retryTicket === undefined) throw new Error("reconciled room is not claimable");
94
+ if (tracker.recordBothRoomTopicsJoined("room-1", retryTicket) !== "subscribed") {
95
+ throw new Error("room did not subscribe");
96
+ }
97
+ const leaveTicket = tracker.unsubscribeRoom("room-1");
98
+ if (leaveTicket === undefined || !tracker.markRoomLeaveComplete("room-1", leaveTicket, "left")) {
99
+ throw new Error("room did not leave");
100
+ }
101
+ break;
102
+ case "subscribed":
103
+ case "join_failed":
104
+ case "rolled_back":
105
+ case "stale":
106
+ break;
107
+ }
108
+ ```
109
+
110
+ ### `validateMemoryTypeForSystem(system, type, traceContext?)`
111
+
112
+ Validates the canonical memory taxonomy — design decisions:
113
+ [`memory-taxonomy-policy.md`](https://github.com/band-ai/band-sdk-core/blob/main/crates/core/docs/memory-taxonomy-policy.md).
114
+ `system` and `type` are wire-name strings (`MemorySystem`/`MemoryType` in
115
+ `index.d.ts` are string-literal types, not classes — there is no runtime
116
+ enum on this boundary, matching how `EventType` stays a plain string in
117
+ `validateEventPayload`). Returns `void` on success. Failure throws a
118
+ native `Error` with the same `.issues`/`.traceContext` contract as
119
+ `validateEventPayload` — an unrecognized `system` and an unrecognized
120
+ `type` are independent issues, both reported when both strings are
121
+ invalid.
122
+
123
+ The public declaration is
124
+ [`index.d.ts`](https://github.com/band-ai/band-sdk-core/blob/main/crates/wasm/index.d.ts).
125
+
126
+ ## Build / test
127
+
128
+ `just check` compiles and lints this crate for the host.
129
+
130
+ ```bash
131
+ just test-wasm # suite in Node
132
+ just pack-npm # Node tarball + require/import consumers
133
+ just build-wasm # Node package output
134
+ ```
135
+
136
+ ## License
137
+
138
+ MIT — see [LICENSE](https://github.com/band-ai/band-sdk-core/blob/main/LICENSE).
package/band_sdk_core.js CHANGED
@@ -348,6 +348,264 @@ class RetryTracker {
348
348
  if (Symbol.dispose) RetryTracker.prototype[Symbol.dispose] = RetryTracker.prototype.free;
349
349
  exports.RetryTracker = RetryTracker;
350
350
 
351
+ /**
352
+ * Transport-independent subscription decisions for one agent session.
353
+ */
354
+ class SubscriptionTracker {
355
+ __destroy_into_raw() {
356
+ const ptr = this.__wbg_ptr;
357
+ this.__wbg_ptr = 0;
358
+ SubscriptionTrackerFinalization.unregister(this);
359
+ return ptr;
360
+ }
361
+ free() {
362
+ const ptr = this.__destroy_into_raw();
363
+ wasm.__wbg_subscriptiontracker_free(ptr, 0);
364
+ }
365
+ /**
366
+ * @param {any} topic
367
+ * @returns {boolean}
368
+ */
369
+ acknowledgeAgentTopicReconciled(topic) {
370
+ const ret = wasm.subscriptiontracker_acknowledgeAgentTopicReconciled(this.__wbg_ptr, topic);
371
+ if (ret[2]) {
372
+ throw takeFromExternrefTable0(ret[1]);
373
+ }
374
+ return ret[0] !== 0;
375
+ }
376
+ /**
377
+ * @param {any} room_id
378
+ * @returns {boolean}
379
+ */
380
+ acknowledgeRoomReconciled(room_id) {
381
+ const ret = wasm.subscriptiontracker_acknowledgeRoomReconciled(this.__wbg_ptr, room_id);
382
+ if (ret[2]) {
383
+ throw takeFromExternrefTable0(ret[1]);
384
+ }
385
+ return ret[0] !== 0;
386
+ }
387
+ /**
388
+ * @param {any} topic
389
+ * @returns {string}
390
+ */
391
+ agentTopicStatus(topic) {
392
+ const ret = wasm.subscriptiontracker_agentTopicStatus(this.__wbg_ptr, topic);
393
+ if (ret[3]) {
394
+ throw takeFromExternrefTable0(ret[2]);
395
+ }
396
+ return getStringFromWasm0(ret[0], ret[1]);
397
+ }
398
+ /**
399
+ * @param {any} topic
400
+ * @returns {bigint | undefined}
401
+ */
402
+ beginAgentTopicJoin(topic) {
403
+ const ret = wasm.subscriptiontracker_beginAgentTopicJoin(this.__wbg_ptr, topic);
404
+ if (ret[3]) {
405
+ throw takeFromExternrefTable0(ret[2]);
406
+ }
407
+ return ret[0] === 0 ? undefined : BigInt.asUintN(64, ret[1]);
408
+ }
409
+ /**
410
+ * @param {any} room_id
411
+ * @returns {bigint | undefined}
412
+ */
413
+ beginRoomSubscribe(room_id) {
414
+ const ret = wasm.subscriptiontracker_beginRoomSubscribe(this.__wbg_ptr, room_id);
415
+ if (ret[3]) {
416
+ throw takeFromExternrefTable0(ret[2]);
417
+ }
418
+ return ret[0] === 0 ? undefined : BigInt.asUintN(64, ret[1]);
419
+ }
420
+ endSession() {
421
+ wasm.subscriptiontracker_endSession(this.__wbg_ptr);
422
+ }
423
+ /**
424
+ * @param {any} topic
425
+ * @param {bigint} ticket
426
+ * @returns {boolean}
427
+ */
428
+ isAgentTopicClaimCurrent(topic, ticket) {
429
+ const ret = wasm.subscriptiontracker_isAgentTopicClaimCurrent(this.__wbg_ptr, topic, ticket);
430
+ if (ret[2]) {
431
+ throw takeFromExternrefTable0(ret[1]);
432
+ }
433
+ return ret[0] !== 0;
434
+ }
435
+ /**
436
+ * @param {any} topic
437
+ * @returns {boolean}
438
+ */
439
+ isAgentTopicJoined(topic) {
440
+ const ret = wasm.subscriptiontracker_isAgentTopicJoined(this.__wbg_ptr, topic);
441
+ if (ret[2]) {
442
+ throw takeFromExternrefTable0(ret[1]);
443
+ }
444
+ return ret[0] !== 0;
445
+ }
446
+ /**
447
+ * @param {any} room_id
448
+ * @param {bigint} ticket
449
+ * @returns {boolean}
450
+ */
451
+ isRoomClaimCurrent(room_id, ticket) {
452
+ const ret = wasm.subscriptiontracker_isRoomClaimCurrent(this.__wbg_ptr, room_id, ticket);
453
+ if (ret[2]) {
454
+ throw takeFromExternrefTable0(ret[1]);
455
+ }
456
+ return ret[0] !== 0;
457
+ }
458
+ /**
459
+ * @param {any} room_id
460
+ * @returns {boolean}
461
+ */
462
+ isRoomSubscribed(room_id) {
463
+ const ret = wasm.subscriptiontracker_isRoomSubscribed(this.__wbg_ptr, room_id);
464
+ if (ret[2]) {
465
+ throw takeFromExternrefTable0(ret[1]);
466
+ }
467
+ return ret[0] !== 0;
468
+ }
469
+ /**
470
+ * @returns {string[]}
471
+ */
472
+ joinedAgentTopics() {
473
+ const ret = wasm.subscriptiontracker_joinedAgentTopics(this.__wbg_ptr);
474
+ var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]);
475
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
476
+ return v1;
477
+ }
478
+ /**
479
+ * @param {any} topic
480
+ * @returns {bigint | undefined}
481
+ */
482
+ leaveAgentTopic(topic) {
483
+ const ret = wasm.subscriptiontracker_leaveAgentTopic(this.__wbg_ptr, topic);
484
+ if (ret[3]) {
485
+ throw takeFromExternrefTable0(ret[2]);
486
+ }
487
+ return ret[0] === 0 ? undefined : BigInt.asUintN(64, ret[1]);
488
+ }
489
+ /**
490
+ * @param {any} topic
491
+ * @param {bigint} ticket
492
+ * @param {any} outcome
493
+ * @returns {boolean}
494
+ */
495
+ markAgentTopicLeaveComplete(topic, ticket, outcome) {
496
+ const ret = wasm.subscriptiontracker_markAgentTopicLeaveComplete(this.__wbg_ptr, topic, ticket, outcome);
497
+ if (ret[2]) {
498
+ throw takeFromExternrefTable0(ret[1]);
499
+ }
500
+ return ret[0] !== 0;
501
+ }
502
+ /**
503
+ * @param {any} room_id
504
+ * @param {bigint} ticket
505
+ * @param {any} outcome
506
+ * @returns {boolean}
507
+ */
508
+ markRoomLeaveComplete(room_id, ticket, outcome) {
509
+ const ret = wasm.subscriptiontracker_markRoomLeaveComplete(this.__wbg_ptr, room_id, ticket, outcome);
510
+ if (ret[2]) {
511
+ throw takeFromExternrefTable0(ret[1]);
512
+ }
513
+ return ret[0] !== 0;
514
+ }
515
+ constructor() {
516
+ const ret = wasm.subscriptiontracker_new();
517
+ this.__wbg_ptr = ret;
518
+ SubscriptionTrackerFinalization.register(this, this.__wbg_ptr, this);
519
+ return this;
520
+ }
521
+ onReconnected() {
522
+ wasm.subscriptiontracker_onReconnected(this.__wbg_ptr);
523
+ }
524
+ /**
525
+ * @param {any} topic
526
+ * @param {bigint} ticket
527
+ * @param {boolean} joined
528
+ * @returns {boolean}
529
+ */
530
+ recordAgentTopicJoin(topic, ticket, joined) {
531
+ const ret = wasm.subscriptiontracker_recordAgentTopicJoin(this.__wbg_ptr, topic, ticket, joined);
532
+ if (ret[2]) {
533
+ throw takeFromExternrefTable0(ret[1]);
534
+ }
535
+ return ret[0] !== 0;
536
+ }
537
+ /**
538
+ * @param {any} room_id
539
+ * @param {bigint} ticket
540
+ * @returns {string}
541
+ */
542
+ recordBothRoomTopicsJoined(room_id, ticket) {
543
+ const ret = wasm.subscriptiontracker_recordBothRoomTopicsJoined(this.__wbg_ptr, room_id, ticket);
544
+ if (ret[3]) {
545
+ throw takeFromExternrefTable0(ret[2]);
546
+ }
547
+ return getStringFromWasm0(ret[0], ret[1]);
548
+ }
549
+ /**
550
+ * @param {any} room_id
551
+ * @param {bigint} ticket
552
+ * @returns {string}
553
+ */
554
+ recordChatRoomJoinFailed(room_id, ticket) {
555
+ const ret = wasm.subscriptiontracker_recordChatRoomJoinFailed(this.__wbg_ptr, room_id, ticket);
556
+ if (ret[3]) {
557
+ throw takeFromExternrefTable0(ret[2]);
558
+ }
559
+ return getStringFromWasm0(ret[0], ret[1]);
560
+ }
561
+ /**
562
+ * @param {any} room_id
563
+ * @param {bigint} ticket
564
+ * @param {boolean} chatRoomLeft
565
+ * @returns {string}
566
+ */
567
+ recordRoomParticipantsJoinFailed(room_id, ticket, chatRoomLeft) {
568
+ const ret = wasm.subscriptiontracker_recordRoomParticipantsJoinFailed(this.__wbg_ptr, room_id, ticket, chatRoomLeft);
569
+ if (ret[3]) {
570
+ throw takeFromExternrefTable0(ret[2]);
571
+ }
572
+ return getStringFromWasm0(ret[0], ret[1]);
573
+ }
574
+ /**
575
+ * @param {any} room_id
576
+ * @returns {string}
577
+ */
578
+ roomStatus(room_id) {
579
+ const ret = wasm.subscriptiontracker_roomStatus(this.__wbg_ptr, room_id);
580
+ if (ret[3]) {
581
+ throw takeFromExternrefTable0(ret[2]);
582
+ }
583
+ return getStringFromWasm0(ret[0], ret[1]);
584
+ }
585
+ /**
586
+ * @returns {string[]}
587
+ */
588
+ subscribedRoomIds() {
589
+ const ret = wasm.subscriptiontracker_subscribedRoomIds(this.__wbg_ptr);
590
+ var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]);
591
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
592
+ return v1;
593
+ }
594
+ /**
595
+ * @param {any} room_id
596
+ * @returns {bigint | undefined}
597
+ */
598
+ unsubscribeRoom(room_id) {
599
+ const ret = wasm.subscriptiontracker_unsubscribeRoom(this.__wbg_ptr, room_id);
600
+ if (ret[3]) {
601
+ throw takeFromExternrefTable0(ret[2]);
602
+ }
603
+ return ret[0] === 0 ? undefined : BigInt.asUintN(64, ret[1]);
604
+ }
605
+ }
606
+ if (Symbol.dispose) SubscriptionTracker.prototype[Symbol.dispose] = SubscriptionTracker.prototype.free;
607
+ exports.SubscriptionTracker = SubscriptionTracker;
608
+
351
609
  /**
352
610
  * Validate and normalize an inbound platform event payload.
353
611
  *
@@ -650,6 +908,9 @@ const ParticipantRosterFinalization = (typeof FinalizationRegistry === 'undefine
650
908
  const RetryTrackerFinalization = (typeof FinalizationRegistry === 'undefined')
651
909
  ? { register: () => {}, unregister: () => {} }
652
910
  : new FinalizationRegistry(ptr => wasm.__wbg_retrytracker_free(ptr, 1));
911
+ const SubscriptionTrackerFinalization = (typeof FinalizationRegistry === 'undefined')
912
+ ? { register: () => {}, unregister: () => {} }
913
+ : new FinalizationRegistry(ptr => wasm.__wbg_subscriptiontracker_free(ptr, 1));
653
914
 
654
915
  function addToExternrefTable0(obj) {
655
916
  const idx = wasm.__externref_table_alloc();
Binary file
package/index.d.ts CHANGED
@@ -145,3 +145,53 @@ export class ParticipantRoster {
145
145
  changed(): boolean;
146
146
  markSent(): void;
147
147
  }
148
+
149
+ export type RoomSubscribeResult =
150
+ | "subscribed"
151
+ | "join_failed"
152
+ | "rolled_back"
153
+ | "rollback_failed"
154
+ | "stale";
155
+
156
+ export type RoomStatus =
157
+ | "absent"
158
+ | "pending"
159
+ | "subscribed"
160
+ | "leaving"
161
+ | "needs_reconciliation";
162
+
163
+ export type AgentTopicStatus =
164
+ | "absent"
165
+ | "pending"
166
+ | "joined"
167
+ | "leaving"
168
+ | "needs_reconciliation";
169
+
170
+ export type LeaveOutcome = "left" | "failed" | "unknown";
171
+
172
+ /** Transport-independent subscription decisions for one agent session. */
173
+ export class SubscriptionTracker {
174
+ constructor();
175
+ beginRoomSubscribe(roomId: string): bigint | undefined;
176
+ recordChatRoomJoinFailed(roomId: string, ticket: bigint): RoomSubscribeResult;
177
+ recordBothRoomTopicsJoined(roomId: string, ticket: bigint): RoomSubscribeResult;
178
+ recordRoomParticipantsJoinFailed(roomId: string, ticket: bigint, chatRoomLeft: boolean): RoomSubscribeResult;
179
+ isRoomSubscribed(roomId: string): boolean;
180
+ isRoomClaimCurrent(roomId: string, ticket: bigint): boolean;
181
+ roomStatus(roomId: string): RoomStatus;
182
+ unsubscribeRoom(roomId: string): bigint | undefined;
183
+ markRoomLeaveComplete(roomId: string, ticket: bigint, outcome: LeaveOutcome): boolean;
184
+ acknowledgeRoomReconciled(roomId: string): boolean;
185
+ subscribedRoomIds(): string[];
186
+ onReconnected(): void;
187
+ endSession(): void;
188
+ beginAgentTopicJoin(topic: string): bigint | undefined;
189
+ recordAgentTopicJoin(topic: string, ticket: bigint, joined: boolean): boolean;
190
+ isAgentTopicJoined(topic: string): boolean;
191
+ isAgentTopicClaimCurrent(topic: string, ticket: bigint): boolean;
192
+ agentTopicStatus(topic: string): AgentTopicStatus;
193
+ leaveAgentTopic(topic: string): bigint | undefined;
194
+ markAgentTopicLeaveComplete(topic: string, ticket: bigint, outcome: LeaveOutcome): boolean;
195
+ acknowledgeAgentTopicReconciled(topic: string): boolean;
196
+ joinedAgentTopics(): string[];
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@band-ai/band-sdk-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared Band event-payload validation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "band_sdk_core.js",
12
12
  "band_sdk_core_bg.wasm",
13
- "index.d.ts"
13
+ "index.d.ts",
14
+ "README.md"
14
15
  ],
15
16
  "main": "./band_sdk_core.js",
16
17
  "types": "./index.d.ts",