@band-ai/sdk 0.2.1 → 0.3.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.
Files changed (37) hide show
  1. package/dist/{ClaudeSDKAdapter-Cna5P1dR.d.ts → ClaudeSDKAdapter-CXud2DBE.d.ts} +2 -1
  2. package/dist/{ClaudeSDKAdapter-BY4zEbCl.d.cts → ClaudeSDKAdapter-Cx6zhSaG.d.cts} +2 -1
  3. package/dist/adapters.cjs +208 -115
  4. package/dist/adapters.d.cts +8 -4
  5. package/dist/adapters.d.ts +8 -4
  6. package/dist/adapters.js +105 -43
  7. package/dist/chunk-F63TPOI3.js +29 -0
  8. package/dist/{chunk-3BKAC4OZ.js → chunk-JADCAMZY.js} +8 -0
  9. package/dist/{chunk-N2QTUMGO.js → chunk-JJF44XWA.js} +365 -249
  10. package/dist/{chunk-TLBCKDQO.js → chunk-K5KAEYPF.js} +2 -2
  11. package/dist/{chunk-ZZJRRBPQ.js → chunk-KXJ74J73.js} +1 -1
  12. package/dist/{chunk-NMZLX7TP.js → chunk-MQBBWYRP.js} +52 -45
  13. package/dist/{chunk-RV76SKFT.js → chunk-MQY74R7D.js} +2 -20
  14. package/dist/{chunk-LBWKISBS.js → chunk-OH6VVVKT.js} +21 -0
  15. package/dist/{chunk-EAI2M3WM.js → chunk-W5IAI6Z4.js} +1 -1
  16. package/dist/{chunk-4IAXZBXX.js → chunk-WA7CHXUS.js} +2 -2
  17. package/dist/{chunk-NT7TBFX2.js → chunk-XWHWJP4I.js} +2 -2
  18. package/dist/{chunk-WLCM2OXG.js → chunk-YJ42AZO4.js} +1 -1
  19. package/dist/converters.js +6 -5
  20. package/dist/index.cjs +405 -277
  21. package/dist/index.d.cts +5 -5
  22. package/dist/index.d.ts +5 -5
  23. package/dist/index.js +12 -11
  24. package/dist/mcp-claude.js +4 -3
  25. package/dist/mcp.js +5 -4
  26. package/dist/rest.js +3 -3
  27. package/dist/runtime.cjs +387 -362
  28. package/dist/runtime.d.cts +10 -39
  29. package/dist/runtime.d.ts +10 -39
  30. package/dist/runtime.js +20 -142
  31. package/dist/{schemas-BotL3ZLH.d.cts → schemas-BgJhlqqO.d.cts} +57 -13
  32. package/dist/{schemas-DBsC4J1g.d.ts → schemas-C4mK1WSs.d.ts} +57 -13
  33. package/dist/testing.cjs +11 -0
  34. package/dist/testing.js +8 -1
  35. package/dist/{types-D958TC71.d.cts → types-BNNtOXu3.d.cts} +1 -1
  36. package/dist/{types-BTN_ZETM.d.ts → types-DFUtpxah.d.ts} +1 -1
  37. package/package.json +2 -2
package/dist/runtime.cjs CHANGED
@@ -41,10 +41,11 @@ __export(runtime_exports, {
41
41
  MCP_TOOL_PREFIX: () => MCP_TOOL_PREFIX,
42
42
  MEMORY_SECTION: () => MEMORY_SECTION,
43
43
  MEMORY_TOOL_NAMES: () => MEMORY_TOOL_NAMES,
44
- ParticipantRoster: () => import_band_sdk_core3.ParticipantRoster,
44
+ ParticipantRoster: () => import_band_sdk_core4.ParticipantRoster,
45
45
  PlatformRuntime: () => PlatformRuntime,
46
- RetryTracker: () => import_band_sdk_core3.RetryTracker,
46
+ RetryTracker: () => import_band_sdk_core4.RetryTracker,
47
47
  RoomPresence: () => RoomPresence,
48
+ RoomRoster: () => import_band_sdk_core4.RoomRoster,
48
49
  SYNTHETIC_CONTACT_EVENTS_SENDER_ID: () => SYNTHETIC_CONTACT_EVENTS_SENDER_ID,
49
50
  SYNTHETIC_CONTACT_EVENTS_SENDER_NAME: () => SYNTHETIC_CONTACT_EVENTS_SENDER_NAME,
50
51
  SYNTHETIC_SENDER_TYPE: () => SYNTHETIC_SENDER_TYPE,
@@ -58,6 +59,7 @@ __export(runtime_exports, {
58
59
  getToolDescription: () => getToolDescription,
59
60
  isChatEventType: () => isChatEventType,
60
61
  mcpToolNames: () => mcpToolNames,
62
+ mentionSubjectsFromMetadata: () => mentionSubjectsFromMetadata,
61
63
  renderSystemPrompt: () => renderSystemPrompt,
62
64
  replaceUuidMentions: () => replaceUuidMentions,
63
65
  runWithGracefulShutdown: () => runWithGracefulShutdown
@@ -138,6 +140,26 @@ var RuntimeStateError = class extends BandSdkError {
138
140
  function isMetadataMap(value) {
139
141
  return value !== null && typeof value === "object" && !Array.isArray(value);
140
142
  }
143
+ function mentionSubjectsFromMetadata(metadata) {
144
+ const mentions = metadata?.mentions;
145
+ if (!Array.isArray(mentions)) {
146
+ return [];
147
+ }
148
+ const subjects = [];
149
+ for (const mention of mentions) {
150
+ if (!isMetadataMap(mention) || typeof mention.id !== "string") {
151
+ continue;
152
+ }
153
+ const label = [mention.handle, mention.username, mention.name].find(
154
+ (value) => typeof value === "string" && value.trim().length > 0
155
+ );
156
+ if (label === void 0) {
157
+ continue;
158
+ }
159
+ subjects.push({ id: mention.id, handle: label });
160
+ }
161
+ return subjects;
162
+ }
141
163
  function replaceUuidMentions(content, participants) {
142
164
  if (!content || participants.length === 0) {
143
165
  return content;
@@ -1234,6 +1256,13 @@ function isToolExecutorError(value) {
1234
1256
  const candidate = value;
1235
1257
  return candidate.ok === false && typeof candidate.errorType === "string" && TOOL_EXECUTOR_ERROR_TYPES.includes(candidate.errorType) && typeof candidate.toolName === "string" && typeof candidate.message === "string" && typeof candidate.legacyMessage === "string";
1236
1258
  }
1259
+ function isStructuredToolFailure(value) {
1260
+ if (!value || typeof value !== "object") {
1261
+ return false;
1262
+ }
1263
+ const payload = value;
1264
+ return payload.ok === false && typeof payload.message === "string";
1265
+ }
1237
1266
  var DEFAULT_AGENT_TOOLS_CAPABILITIES = {
1238
1267
  peers: true,
1239
1268
  contacts: true,
@@ -1711,10 +1740,12 @@ var AgentTools = class {
1711
1740
  roster;
1712
1741
  adapterTools;
1713
1742
  toolHandlers;
1743
+ logger;
1714
1744
  constructor(options) {
1715
1745
  this.roomId = options.roomId;
1716
1746
  this.rest = options.rest;
1717
1747
  this.roster = options.roster ?? new import_band_sdk_core.ParticipantRoster();
1748
+ this.logger = options.logger ?? new NoopLogger();
1718
1749
  this.capabilities = {
1719
1750
  ...DEFAULT_AGENT_TOOLS_CAPABILITIES,
1720
1751
  ...options.capabilities
@@ -1744,14 +1775,23 @@ var AgentTools = class {
1744
1775
  }
1745
1776
  async sendEvent(content, messageType, metadata) {
1746
1777
  assertChatEventType(messageType);
1747
- return this.rest.createChatEvent(
1748
- this.roomId,
1749
- {
1750
- content,
1751
- messageType,
1752
- metadata
1778
+ try {
1779
+ return await this.rest.createChatEvent(
1780
+ this.roomId,
1781
+ {
1782
+ content,
1783
+ messageType,
1784
+ metadata
1785
+ }
1786
+ );
1787
+ } catch (error) {
1788
+ try {
1789
+ this.logger.warn("chat event send failed", { roomId: this.roomId, messageType, error });
1790
+ } catch {
1753
1791
  }
1754
- );
1792
+ const message = error instanceof Error ? error.message : String(error);
1793
+ return { ok: false, status: "failed", message };
1794
+ }
1755
1795
  }
1756
1796
  async createChatroom(taskId) {
1757
1797
  const room = await this.rest.createChat(taskId, DEFAULT_REQUEST_OPTIONS);
@@ -1854,7 +1894,16 @@ var AgentTools = class {
1854
1894
  });
1855
1895
  }
1856
1896
  try {
1857
- return await handler(arguments_);
1897
+ const result = await handler(arguments_);
1898
+ if (toolName === "band_send_event" && isStructuredToolFailure(result) && !isToolExecutorError(result)) {
1899
+ return createToolExecutorError({
1900
+ errorType: "ToolExecutionError",
1901
+ toolName,
1902
+ message: result.message,
1903
+ legacyMessage: `Error executing ${toolName}: ${result.message}`
1904
+ });
1905
+ }
1906
+ return result;
1858
1907
  } catch (error) {
1859
1908
  if (isToolExecutorError(error)) {
1860
1909
  return error;
@@ -2555,7 +2604,8 @@ var ExecutionContext = class {
2555
2604
  roomId: this.roomId,
2556
2605
  rest: this.link.rest,
2557
2606
  roster: this.roster,
2558
- capabilities: this.link.capabilities
2607
+ capabilities: this.link.capabilities,
2608
+ logger: options.logger ?? new NoopLogger()
2559
2609
  });
2560
2610
  this.adapterTools = this.tools.getAdapterTools();
2561
2611
  }
@@ -2803,70 +2853,278 @@ var ExecutionContext = class {
2803
2853
  }
2804
2854
  };
2805
2855
 
2856
+ // src/runtime/rooms/RoomPresence.ts
2857
+ var import_band_sdk_core3 = require("@band-ai/band-sdk-core");
2858
+
2806
2859
  // src/runtime/rooms/subscriptions.ts
2807
- function hasRoomId(roomId) {
2808
- return typeof roomId === "string" && roomId.length > 0;
2860
+ async function hydrateExistingRooms(options) {
2861
+ try {
2862
+ const rooms = await options.link.listAllChats(void 0, options.requestOptions);
2863
+ for (const room of rooms) {
2864
+ const roomId = typeof room.id === "string" ? room.id : null;
2865
+ if (!roomId) {
2866
+ continue;
2867
+ }
2868
+ if (options.roomFilter && !options.roomFilter(room)) {
2869
+ continue;
2870
+ }
2871
+ await options.onRoom(roomId, room);
2872
+ }
2873
+ } catch (error) {
2874
+ if (error instanceof UnsupportedFeatureError) {
2875
+ return;
2876
+ }
2877
+ if (options.onError) {
2878
+ await options.onError(error);
2879
+ return;
2880
+ }
2881
+ throw error;
2882
+ }
2809
2883
  }
2810
- async function trackRoomJoin(options) {
2811
- if (!hasRoomId(options.roomId)) {
2812
- return false;
2884
+
2885
+ // src/runtime/rooms/RoomPresence.ts
2886
+ var RoomPresence = class {
2887
+ roster = new import_band_sdk_core3.RoomRoster();
2888
+ onRoomJoined = null;
2889
+ onRoomLeft = null;
2890
+ onRoomEvent = null;
2891
+ onContactEvent = null;
2892
+ link;
2893
+ roomFilter;
2894
+ autoSubscribeExistingRooms;
2895
+ logger;
2896
+ eventController = null;
2897
+ eventTask = null;
2898
+ contactsSubscribed = false;
2899
+ lifecycle = Promise.resolve();
2900
+ admissionInFlight = /* @__PURE__ */ new Map();
2901
+ // Read once by `admitRoomOrThrow` right after a failed admission; a
2902
+ // subsequent successful subscribe clears it so a caller never attributes
2903
+ // today's failure to a stale reason from an earlier attempt.
2904
+ lastSubscribeError = /* @__PURE__ */ new Map();
2905
+ constructor(options) {
2906
+ this.link = options.link;
2907
+ this.roomFilter = options.roomFilter;
2908
+ this.autoSubscribeExistingRooms = options.autoSubscribeExistingRooms ?? true;
2909
+ this.logger = options.logger ?? new NoopLogger();
2813
2910
  }
2814
- if (options.roomFilter && !options.roomFilter(options.payload)) {
2815
- return false;
2911
+ async start() {
2912
+ return this.serialize(() => this.startBody());
2816
2913
  }
2817
- if (options.trackedRooms.has(options.roomId)) {
2818
- return false;
2914
+ async stop() {
2915
+ return this.serialize(() => this.stopBody());
2819
2916
  }
2820
- await options.link.subscribeRoom(options.roomId);
2821
- options.trackedRooms.add(options.roomId);
2822
- if (options.onJoined) {
2823
- await options.onJoined(options.roomId, options.payload);
2917
+ async [Symbol.asyncDispose]() {
2918
+ await this.stop();
2824
2919
  }
2825
- return true;
2826
- }
2827
- async function trackRoomLeave(options) {
2828
- if (!hasRoomId(options.roomId) || !options.trackedRooms.has(options.roomId)) {
2829
- return false;
2920
+ abortEventLoop() {
2921
+ this.eventController?.abort();
2830
2922
  }
2831
- await options.link.unsubscribeRoom(options.roomId);
2832
- options.trackedRooms.delete(options.roomId);
2833
- if (options.onLeft) {
2834
- await options.onLeft(options.roomId);
2923
+ async waitUntilStopped() {
2924
+ await this.eventTask;
2835
2925
  }
2836
- return true;
2837
- }
2838
- async function hydrateTrackedRooms(options) {
2839
- const pageSize = options.pageSize ?? 100;
2840
- const maxPages = options.maxPages ?? 100;
2841
- try {
2842
- const rooms = await options.link.listAllChats(
2843
- { pageSize, maxPages },
2844
- options.requestOptions
2926
+ async admitRoom(roomId, payload, notify = true) {
2927
+ const ticket = this.roster.beginRoomAdmission(roomId, true);
2928
+ const admitted = ticket === void 0 ? await this.awaitInFlightAdmission(roomId) : await this.performAdmission(roomId, ticket);
2929
+ if (admitted && notify) {
2930
+ await this.onRoomJoined?.(roomId, payload);
2931
+ }
2932
+ return admitted;
2933
+ }
2934
+ /** Admit a room for a caller that needs to know *why* a failed subscribe happened, not just that it did. */
2935
+ async admitRoomOrThrow(roomId) {
2936
+ if (!await this.admitRoom(roomId, {}, false)) {
2937
+ throw new TransportError(`Failed to subscribe to room ${roomId}`, this.lastSubscribeError.get(roomId));
2938
+ }
2939
+ }
2940
+ async awaitInFlightAdmission(roomId) {
2941
+ const inFlight = this.admissionInFlight.get(roomId);
2942
+ return inFlight ? await inFlight : this.roster.roomMembership(roomId) === "admitted";
2943
+ }
2944
+ async performAdmission(roomId, ticket) {
2945
+ const admission = this.completeAdmission(roomId, ticket);
2946
+ this.admissionInFlight.set(roomId, admission);
2947
+ try {
2948
+ return await admission;
2949
+ } finally {
2950
+ if (this.admissionInFlight.get(roomId) === admission) {
2951
+ this.admissionInFlight.delete(roomId);
2952
+ }
2953
+ }
2954
+ }
2955
+ async completeAdmission(roomId, ticket) {
2956
+ let succeeded = false;
2957
+ let admitted = false;
2958
+ let subscribeError;
2959
+ try {
2960
+ await this.link.subscribeRoom(roomId);
2961
+ succeeded = true;
2962
+ } catch (error) {
2963
+ this.logger.warn("RoomPresence failed to subscribe room", { roomId, error });
2964
+ subscribeError = error;
2965
+ } finally {
2966
+ admitted = this.roster.recordRoomAdmission(roomId, ticket, succeeded);
2967
+ }
2968
+ const roomIsAdmitted = admitted || this.roster.roomMembership(roomId) === "admitted";
2969
+ if (succeeded && !admitted && !roomIsAdmitted) {
2970
+ this.logger.debug("RoomPresence admission ticket went stale", { roomId });
2971
+ await this.unsubscribeRoom(roomId);
2972
+ }
2973
+ if (roomIsAdmitted) {
2974
+ this.lastSubscribeError.delete(roomId);
2975
+ } else if (subscribeError !== void 0) {
2976
+ this.lastSubscribeError.set(roomId, subscribeError);
2977
+ }
2978
+ return roomIsAdmitted;
2979
+ }
2980
+ async serialize(body) {
2981
+ const run = this.lifecycle.then(body, body);
2982
+ this.lifecycle = run.then(
2983
+ () => void 0,
2984
+ () => void 0
2845
2985
  );
2846
- for (const room of rooms) {
2847
- await trackRoomJoin({
2848
- link: options.link,
2849
- roomId: typeof room.id === "string" ? room.id : null,
2850
- payload: room,
2851
- trackedRooms: options.trackedRooms,
2852
- roomFilter: options.roomFilter,
2853
- onJoined: options.onJoined
2986
+ return run;
2987
+ }
2988
+ async startBody() {
2989
+ if (this.eventTask) {
2990
+ throw new RuntimeStateError("RoomPresence is already started");
2991
+ }
2992
+ if (!this.link.isConnected()) {
2993
+ await this.link.connect();
2994
+ }
2995
+ const contactsReady = this.subscribeContacts();
2996
+ try {
2997
+ await this.link.subscribeAgentRooms();
2998
+ } catch (error) {
2999
+ this.logger.warn("RoomPresence failed to subscribe agent_rooms channel, continuing without it", {
3000
+ error
2854
3001
  });
2855
3002
  }
2856
- } catch (error) {
2857
- if (error instanceof UnsupportedFeatureError) {
3003
+ if (this.autoSubscribeExistingRooms) {
3004
+ await this.subscribeExistingRooms();
3005
+ }
3006
+ await contactsReady;
3007
+ this.eventController = new AbortController();
3008
+ this.eventTask = this.consumeEvents(this.eventController.signal);
3009
+ }
3010
+ async subscribeContacts() {
3011
+ if (!this.link.capabilities.contacts) {
2858
3012
  return;
2859
3013
  }
2860
- if (options.onError) {
2861
- await options.onError(error);
3014
+ try {
3015
+ await this.link.subscribeAgentContacts();
3016
+ this.contactsSubscribed = true;
3017
+ } catch (error) {
3018
+ this.logger.warn("RoomPresence failed to subscribe agent_contacts channel, continuing without it", {
3019
+ error
3020
+ });
3021
+ }
3022
+ }
3023
+ async stopBody() {
3024
+ this.abortEventLoop();
3025
+ await this.eventTask?.catch(() => void 0);
3026
+ this.eventTask = null;
3027
+ this.eventController = null;
3028
+ if (this.contactsSubscribed) {
3029
+ try {
3030
+ await this.link.unsubscribeAgentContacts();
3031
+ } catch (error) {
3032
+ this.logger.warn("RoomPresence failed to unsubscribe agent_contacts channel", { error });
3033
+ } finally {
3034
+ this.contactsSubscribed = false;
3035
+ }
3036
+ }
3037
+ const roomIds = this.roster.trackedRoomIds();
3038
+ this.roster.clear();
3039
+ this.lastSubscribeError.clear();
3040
+ await Promise.all(
3041
+ roomIds.map(async (roomId) => {
3042
+ await this.unsubscribeRoom(roomId);
3043
+ await this.onRoomLeft?.(roomId);
3044
+ })
3045
+ );
3046
+ }
3047
+ async consumeEvents(signal) {
3048
+ while (!signal.aborted) {
3049
+ const event = await this.link.nextEvent(signal);
3050
+ if (!event) {
3051
+ return;
3052
+ }
3053
+ switch (event.type) {
3054
+ case "room_added":
3055
+ await this.handleRoomAdded(event.roomId, event.payload);
3056
+ break;
3057
+ case "room_removed":
3058
+ case "room_deleted":
3059
+ await this.handleRoomRemoved(event.roomId);
3060
+ break;
3061
+ case "contact_request_received":
3062
+ case "contact_request_updated":
3063
+ case "contact_added":
3064
+ case "contact_removed":
3065
+ await this.onContactEvent?.(event);
3066
+ break;
3067
+ case "message_created":
3068
+ case "participant_added":
3069
+ case "participant_removed":
3070
+ if (event.roomId && this.roster.roomMembership(event.roomId) === "admitted") {
3071
+ await this.onRoomEvent?.(event.roomId, event);
3072
+ }
3073
+ break;
3074
+ default:
3075
+ assertNever2(event);
3076
+ }
3077
+ }
3078
+ }
3079
+ async handleRoomAdded(roomId, payload) {
3080
+ if (!roomId) {
2862
3081
  return;
2863
3082
  }
2864
- throw error;
3083
+ if (this.roomFilter && !this.roomFilter(payload)) {
3084
+ return;
3085
+ }
3086
+ await this.admitRoom(roomId, payload);
3087
+ }
3088
+ async handleRoomRemoved(roomId) {
3089
+ if (!roomId) {
3090
+ return;
3091
+ }
3092
+ await this.unsubscribeRoom(roomId);
3093
+ this.lastSubscribeError.delete(roomId);
3094
+ if (!this.roster.recordRoomRemoved(roomId)) {
3095
+ this.logger.debug("RoomPresence ignoring removal for untracked room", { roomId });
3096
+ return;
3097
+ }
3098
+ await this.onRoomLeft?.(roomId);
3099
+ }
3100
+ async unsubscribeRoom(roomId) {
3101
+ try {
3102
+ await this.link.unsubscribeRoom(roomId);
3103
+ } catch (error) {
3104
+ this.logger.warn("RoomPresence failed to unsubscribe room", { roomId, error });
3105
+ }
2865
3106
  }
3107
+ async subscribeExistingRooms() {
3108
+ await hydrateExistingRooms({
3109
+ link: this.link,
3110
+ roomFilter: this.roomFilter,
3111
+ requestOptions: DEFAULT_REQUEST_OPTIONS,
3112
+ onRoom: async (roomId, payload) => {
3113
+ await this.admitRoom(roomId, payload);
3114
+ },
3115
+ onError: (error) => {
3116
+ this.logger.warn("RoomPresence failed to subscribe existing rooms", { error });
3117
+ }
3118
+ });
3119
+ }
3120
+ };
3121
+ function assertNever2(value) {
3122
+ throw new Error(`Unhandled platform event: ${JSON.stringify(value)}`);
2866
3123
  }
2867
3124
 
2868
3125
  // src/runtime/rooms/AgentRuntime.ts
2869
3126
  var AgentRuntime = class {
3127
+ presence;
2870
3128
  link;
2871
3129
  agentId;
2872
3130
  onExecute;
@@ -2877,19 +3135,14 @@ var AgentRuntime = class {
2877
3135
  onParticipantAdded;
2878
3136
  onParticipantRemoved;
2879
3137
  onError;
2880
- roomFilter;
2881
3138
  contextFactory;
2882
3139
  sessionConfig;
2883
- autoSubscribeExistingRooms;
2884
- subscribedRooms = /* @__PURE__ */ new Set();
2885
3140
  contexts = /* @__PURE__ */ new Map();
2886
3141
  executions = /* @__PURE__ */ new Map();
2887
3142
  executionWatchers = /* @__PURE__ */ new Map();
2888
3143
  logger;
2889
3144
  running = false;
2890
3145
  stopping = false;
2891
- stopController = new AbortController();
2892
- consumeTask = null;
2893
3146
  fatalError = null;
2894
3147
  constructor(options) {
2895
3148
  this.link = options.link;
@@ -2903,7 +3156,6 @@ var AgentRuntime = class {
2903
3156
  this.onContactEvent = options.onContactEvent;
2904
3157
  this.onParticipantAdded = options.onParticipantAdded;
2905
3158
  this.onParticipantRemoved = options.onParticipantRemoved;
2906
- this.roomFilter = options.roomFilter;
2907
3159
  this.contextFactory = options.contextFactory;
2908
3160
  this.sessionConfig = {
2909
3161
  enableContextCache: options.sessionConfig?.enableContextCache ?? true,
@@ -2912,7 +3164,48 @@ var AgentRuntime = class {
2912
3164
  maxMessageRetries: options.sessionConfig?.maxMessageRetries ?? 1,
2913
3165
  enableContextHydration: options.sessionConfig?.enableContextHydration ?? true
2914
3166
  };
2915
- this.autoSubscribeExistingRooms = options.agentConfig?.autoSubscribeExistingRooms ?? false;
3167
+ this.presence = new RoomPresence({
3168
+ link: this.link,
3169
+ roomFilter: options.roomFilter,
3170
+ autoSubscribeExistingRooms: options.agentConfig?.autoSubscribeExistingRooms ?? false,
3171
+ logger: this.logger
3172
+ });
3173
+ this.presence.onRoomJoined = async (roomId, payload) => {
3174
+ this.getOrCreateExecution(roomId);
3175
+ await this.onRoomJoined?.(roomId, payload);
3176
+ };
3177
+ this.presence.onRoomLeft = async (roomId) => {
3178
+ await this.teardownExecution(roomId);
3179
+ await this.onRoomLeft?.(roomId);
3180
+ };
3181
+ this.presence.onRoomEvent = async (roomId, event) => {
3182
+ switch (event.type) {
3183
+ case "participant_added": {
3184
+ const context = this.getOrCreateContext(roomId);
3185
+ const participant = {
3186
+ id: event.payload.id,
3187
+ name: event.payload.name,
3188
+ type: event.payload.type,
3189
+ handle: event.payload.handle
3190
+ };
3191
+ context.addParticipant(participant);
3192
+ await this.onParticipantAdded?.(roomId, participant);
3193
+ return;
3194
+ }
3195
+ case "participant_removed": {
3196
+ const context = this.getOrCreateContext(roomId);
3197
+ context.removeParticipant(event.payload.id);
3198
+ await this.onParticipantRemoved?.(roomId, event.payload.id);
3199
+ return;
3200
+ }
3201
+ case "message_created":
3202
+ await this.getOrCreateExecution(roomId).enqueue(event);
3203
+ return;
3204
+ default:
3205
+ assertNever3(event);
3206
+ }
3207
+ };
3208
+ this.presence.onContactEvent = this.onContactEvent ?? null;
2916
3209
  }
2917
3210
  async start() {
2918
3211
  if (this.running) {
@@ -2921,45 +3214,17 @@ var AgentRuntime = class {
2921
3214
  this.running = true;
2922
3215
  this.stopping = false;
2923
3216
  this.fatalError = null;
2924
- if (!this.stopController.signal.aborted) {
2925
- this.stopController.abort();
2926
- }
2927
- this.stopController = new AbortController();
2928
3217
  try {
2929
- await this.link.connect();
3218
+ await this.presence.start();
2930
3219
  } catch (error) {
2931
3220
  await this.handleStartFailure();
2932
3221
  throw error;
2933
3222
  }
2934
- try {
2935
- await this.link.subscribeAgentRooms();
2936
- } catch {
2937
- this.logger.warn("AgentRuntime failed to subscribe agent_rooms channel, continuing without it");
2938
- }
2939
- try {
2940
- await this.subscribeExistingRooms();
2941
- } catch (error) {
2942
- await this.handleStartFailure();
2943
- throw error;
2944
- }
2945
- this.consumeTask = this.consumeLoop(this.stopController.signal);
2946
- if (!this.link.capabilities.contacts) {
2947
- return;
2948
- }
2949
- try {
2950
- await this.link.subscribeAgentContacts();
2951
- } catch {
2952
- this.logger.warn("AgentRuntime failed to subscribe agent_contacts channel, continuing without it");
2953
- }
3223
+ void this.presence.waitUntilStopped().catch((error) => this.failRuntime(error, syntheticRuntimeFailureEvent(this.agentId)));
2954
3224
  }
2955
3225
  async handleStartFailure() {
2956
3226
  this.running = false;
2957
3227
  this.stopping = false;
2958
- this.stopController.abort();
2959
- if (this.consumeTask) {
2960
- await this.consumeTask;
2961
- this.consumeTask = null;
2962
- }
2963
3228
  await this.link.disconnect();
2964
3229
  }
2965
3230
  async stop(timeoutMs) {
@@ -2968,34 +3233,25 @@ var AgentRuntime = class {
2968
3233
  }
2969
3234
  this.stopping = true;
2970
3235
  this.running = false;
2971
- this.stopController.abort();
2972
- if (this.consumeTask) {
2973
- await this.consumeTask;
2974
- this.consumeTask = null;
2975
- }
2976
- const deadline = timeoutMs === void 0 ? void 0 : Date.now() + timeoutMs;
3236
+ this.presence.abortEventLoop();
3237
+ await this.presence.waitUntilStopped().catch(() => void 0);
2977
3238
  let graceful = true;
2978
- for (const execution of this.executions.values()) {
2979
- const remaining = deadline === void 0 ? void 0 : Math.max(0, deadline - Date.now());
2980
- const stopped = await execution.stop(remaining);
2981
- if (!stopped) {
2982
- graceful = false;
2983
- }
2984
- }
2985
- for (const roomId of [...this.subscribedRooms]) {
2986
- const remaining = deadline === void 0 ? void 0 : Math.max(0, deadline - Date.now());
2987
- await this.leaveTrackedRoom(roomId, remaining);
2988
- }
3239
+ await Promise.all(
3240
+ [...this.executions].map(async ([roomId, execution]) => {
3241
+ const stopped = await execution.stop(timeoutMs);
3242
+ if (!stopped) {
3243
+ graceful = false;
3244
+ }
3245
+ this.executions.delete(roomId);
3246
+ })
3247
+ );
3248
+ await this.presence.stop();
2989
3249
  for (const roomId of [...this.contexts.keys()]) {
2990
3250
  await this.onSessionCleanup(roomId);
2991
3251
  }
2992
- this.subscribedRooms.clear();
2993
3252
  this.contexts.clear();
2994
3253
  this.executions.clear();
2995
3254
  this.executionWatchers.clear();
2996
- if (this.link.capabilities.contacts) {
2997
- await this.link.unsubscribeAgentContacts();
2998
- }
2999
3255
  await this.link.disconnect();
3000
3256
  if (this.fatalError) {
3001
3257
  throw this.fatalError instanceof Error ? this.fatalError : new Error(String(this.fatalError));
@@ -3006,11 +3262,7 @@ var AgentRuntime = class {
3006
3262
  return this.contexts.get(roomId);
3007
3263
  }
3008
3264
  async waitUntilStopped() {
3009
- if (this.consumeTask) {
3010
- await this.consumeTask;
3011
- } else {
3012
- await this.link.runForever(this.stopController.signal);
3013
- }
3265
+ await this.presence.waitUntilStopped().catch(() => void 0);
3014
3266
  if (this.fatalError) {
3015
3267
  throw this.fatalError instanceof Error ? this.fatalError : new Error(String(this.fatalError));
3016
3268
  }
@@ -3018,97 +3270,19 @@ var AgentRuntime = class {
3018
3270
  getContexts() {
3019
3271
  return [...this.contexts.values()];
3020
3272
  }
3021
- async consumeLoop(signal) {
3022
- while (!signal.aborted) {
3023
- let event;
3024
- try {
3025
- event = await this.link.nextEvent(signal);
3026
- } catch (error) {
3027
- await this.failRuntime(error, syntheticRuntimeFailureEvent(this.agentId));
3028
- return;
3029
- }
3030
- if (!event) {
3031
- return;
3032
- }
3033
- try {
3034
- await this.handleEvent(event);
3035
- } catch (error) {
3036
- await this.failRuntime(error, event);
3037
- return;
3038
- }
3039
- }
3040
- }
3041
- async handleEvent(event) {
3042
- switch (event.type) {
3043
- case "room_added":
3044
- await trackRoomJoin({
3045
- link: this.link,
3046
- roomId: event.roomId,
3047
- payload: event.payload,
3048
- trackedRooms: this.subscribedRooms,
3049
- roomFilter: this.roomFilter,
3050
- onJoined: async (roomId) => {
3051
- this.getOrCreateExecution(roomId);
3052
- await this.onRoomJoined?.(roomId, event.payload);
3053
- }
3054
- });
3055
- return;
3056
- case "room_removed":
3057
- case "room_deleted":
3058
- if (event.roomId) {
3059
- await this.onRoomLeft?.(event.roomId);
3060
- await this.leaveTrackedRoom(event.roomId);
3061
- }
3062
- return;
3063
- case "participant_added":
3064
- if (event.roomId) {
3065
- const context = this.getOrCreateContext(event.roomId);
3066
- const participant = {
3067
- id: event.payload.id,
3068
- name: event.payload.name,
3069
- type: event.payload.type,
3070
- handle: event.payload.handle
3071
- };
3072
- context.addParticipant(participant);
3073
- await this.onParticipantAdded?.(event.roomId, participant);
3074
- }
3075
- return;
3076
- case "participant_removed":
3077
- if (event.roomId) {
3078
- const context = this.getOrCreateContext(event.roomId);
3079
- context.removeParticipant(event.payload.id);
3080
- await this.onParticipantRemoved?.(event.roomId, event.payload.id);
3081
- }
3082
- return;
3083
- case "contact_request_received":
3084
- case "contact_request_updated":
3085
- case "contact_added":
3086
- case "contact_removed":
3087
- await this.onContactEvent?.(event);
3088
- return;
3089
- case "message_created":
3090
- if (!event.roomId) {
3091
- return;
3092
- }
3093
- await this.getOrCreateExecution(event.roomId).enqueue(event);
3094
- return;
3095
- }
3096
- return assertNever2(event);
3097
- }
3098
3273
  async enqueueEvent(roomId, event) {
3099
3274
  await this.getOrCreateExecution(roomId).enqueue(event);
3100
3275
  }
3101
3276
  async bootstrapRoomMessage(roomId, message) {
3102
- await this.link.subscribeRoom(roomId);
3103
- this.subscribedRooms.add(roomId);
3277
+ await this.presence.admitRoomOrThrow(roomId);
3104
3278
  await this.getOrCreateExecution(roomId).bootstrapMessage(message);
3105
3279
  }
3106
3280
  async resetRoomSession(roomId, timeoutMs) {
3281
+ return this.teardownExecution(roomId, timeoutMs);
3282
+ }
3283
+ async teardownExecution(roomId, timeoutMs) {
3107
3284
  const execution = this.executions.get(roomId);
3108
- let graceful = true;
3109
- if (execution) {
3110
- graceful = await execution.stop(timeoutMs);
3111
- }
3285
+ const graceful = execution ? await execution.stop(timeoutMs) : true;
3112
3286
  this.executions.delete(roomId);
3113
3287
  this.contexts.delete(roomId);
3114
3288
  await this.onSessionCleanup(roomId);
@@ -3164,44 +3338,13 @@ var AgentRuntime = class {
3164
3338
  maxMessageRetries: this.sessionConfig.maxMessageRetries,
3165
3339
  enableContextCache: this.sessionConfig.enableContextCache,
3166
3340
  contextCacheTtlSeconds: this.sessionConfig.contextCacheTtlSeconds,
3167
- enableContextHydration: this.sessionConfig.enableContextHydration
3341
+ enableContextHydration: this.sessionConfig.enableContextHydration,
3342
+ logger: this.logger
3168
3343
  };
3169
3344
  const context = this.contextFactory ? this.contextFactory(roomId, defaults) : new ExecutionContext(defaults);
3170
3345
  this.contexts.set(roomId, context);
3171
3346
  return context;
3172
3347
  }
3173
- async subscribeExistingRooms() {
3174
- if (!this.autoSubscribeExistingRooms) {
3175
- return;
3176
- }
3177
- await hydrateTrackedRooms({
3178
- link: this.link,
3179
- trackedRooms: this.subscribedRooms,
3180
- roomFilter: this.roomFilter,
3181
- onJoined: async (roomId, payload) => {
3182
- this.getOrCreateExecution(roomId);
3183
- await this.onRoomJoined?.(roomId, payload);
3184
- },
3185
- onError: async (error) => {
3186
- this.logger.warn("AgentRuntime failed to subscribe existing rooms", {
3187
- error
3188
- });
3189
- }
3190
- });
3191
- }
3192
- async leaveTrackedRoom(roomId, timeoutMs) {
3193
- await trackRoomLeave({
3194
- link: this.link,
3195
- roomId,
3196
- trackedRooms: this.subscribedRooms,
3197
- onLeft: async (leftRoomId) => {
3198
- await this.executions.get(leftRoomId)?.stop(timeoutMs);
3199
- this.contexts.delete(leftRoomId);
3200
- this.executions.delete(leftRoomId);
3201
- await this.onSessionCleanup(leftRoomId);
3202
- }
3203
- });
3204
- }
3205
3348
  async failRuntime(error, event) {
3206
3349
  if (!this.fatalError) {
3207
3350
  this.fatalError = error;
@@ -3213,9 +3356,7 @@ var AgentRuntime = class {
3213
3356
  });
3214
3357
  this.notifyOnError(error, event);
3215
3358
  }
3216
- if (!this.stopController.signal.aborted) {
3217
- this.stopController.abort();
3218
- }
3359
+ this.presence.abortEventLoop();
3219
3360
  }
3220
3361
  notifyOnError(error, event) {
3221
3362
  if (!this.onError) {
@@ -3232,6 +3373,9 @@ var AgentRuntime = class {
3232
3373
  }
3233
3374
  }
3234
3375
  };
3376
+ function assertNever3(value) {
3377
+ throw new Error(`Unhandled room event: ${JSON.stringify(value)}`);
3378
+ }
3235
3379
  function syntheticRuntimeFailureEvent(agentId) {
3236
3380
  return {
3237
3381
  type: "message_created",
@@ -3249,9 +3393,6 @@ function syntheticRuntimeFailureEvent(agentId) {
3249
3393
  }
3250
3394
  };
3251
3395
  }
3252
- function assertNever2(value) {
3253
- throw new Error(`Unhandled platform event: ${JSON.stringify(value)}`);
3254
- }
3255
3396
 
3256
3397
  // src/client/rest/pagination.ts
3257
3398
  var DEFAULT_PAGE_SIZE = 100;
@@ -5293,6 +5434,9 @@ var PlatformRuntime = class {
5293
5434
  this.stopping = false;
5294
5435
  return graceful;
5295
5436
  }
5437
+ async [Symbol.asyncDispose]() {
5438
+ await this.stop();
5439
+ }
5296
5440
  async runForever() {
5297
5441
  if (!this.runtime) {
5298
5442
  throw new RuntimeStateError("Runtime not started");
@@ -5343,129 +5487,8 @@ var PlatformRuntime = class {
5343
5487
  }
5344
5488
  };
5345
5489
 
5346
- // src/runtime/rooms/RoomPresence.ts
5347
- var RoomPresence = class {
5348
- rooms = /* @__PURE__ */ new Set();
5349
- onRoomJoined = null;
5350
- onRoomLeft = null;
5351
- onRoomEvent = null;
5352
- onContactEvent = null;
5353
- link;
5354
- roomFilter;
5355
- autoSubscribeExistingRooms;
5356
- logger;
5357
- eventController = null;
5358
- eventTask = null;
5359
- contactsSubscribed = false;
5360
- constructor(options) {
5361
- this.link = options.link;
5362
- this.roomFilter = options.roomFilter;
5363
- this.autoSubscribeExistingRooms = options.autoSubscribeExistingRooms ?? true;
5364
- this.logger = options.logger ?? new NoopLogger();
5365
- }
5366
- async start() {
5367
- if (this.eventTask) {
5368
- return;
5369
- }
5370
- if (!this.link.isConnected()) {
5371
- await this.link.connect();
5372
- }
5373
- try {
5374
- await this.link.subscribeAgentRooms();
5375
- } catch {
5376
- }
5377
- if (this.autoSubscribeExistingRooms) {
5378
- await this.subscribeExistingRooms();
5379
- }
5380
- if (this.link.capabilities.contacts) {
5381
- await this.link.subscribeAgentContacts();
5382
- this.contactsSubscribed = true;
5383
- }
5384
- this.eventController = new AbortController();
5385
- this.eventTask = this.consumeEvents(this.eventController.signal);
5386
- }
5387
- async stop() {
5388
- this.eventController?.abort();
5389
- await this.eventTask;
5390
- this.eventTask = null;
5391
- this.eventController = null;
5392
- if (this.contactsSubscribed) {
5393
- await this.link.unsubscribeAgentContacts();
5394
- this.contactsSubscribed = false;
5395
- }
5396
- for (const roomId of [...this.rooms]) {
5397
- await trackRoomLeave({
5398
- link: this.link,
5399
- roomId,
5400
- trackedRooms: this.rooms,
5401
- onLeft: this.onRoomLeft ?? void 0
5402
- });
5403
- }
5404
- }
5405
- async consumeEvents(signal) {
5406
- while (!signal.aborted) {
5407
- const event = await this.link.nextEvent(signal);
5408
- if (!event) {
5409
- return;
5410
- }
5411
- switch (event.type) {
5412
- case "room_added":
5413
- await this.handleRoomAdded(event.roomId, event.payload);
5414
- break;
5415
- case "room_removed":
5416
- case "room_deleted":
5417
- await this.handleRoomRemoved(event.roomId);
5418
- break;
5419
- case "contact_request_received":
5420
- case "contact_request_updated":
5421
- case "contact_added":
5422
- case "contact_removed":
5423
- await this.onContactEvent?.(event);
5424
- break;
5425
- default:
5426
- if (event.roomId && this.rooms.has(event.roomId)) {
5427
- await this.onRoomEvent?.(event.roomId, event);
5428
- }
5429
- break;
5430
- }
5431
- }
5432
- }
5433
- async handleRoomAdded(roomId, payload) {
5434
- await trackRoomJoin({
5435
- link: this.link,
5436
- roomId,
5437
- payload,
5438
- trackedRooms: this.rooms,
5439
- roomFilter: this.roomFilter,
5440
- onJoined: this.onRoomJoined ?? void 0
5441
- });
5442
- }
5443
- async handleRoomRemoved(roomId) {
5444
- await trackRoomLeave({
5445
- link: this.link,
5446
- roomId,
5447
- trackedRooms: this.rooms,
5448
- onLeft: this.onRoomLeft ?? void 0
5449
- });
5450
- }
5451
- async subscribeExistingRooms() {
5452
- await hydrateTrackedRooms({
5453
- link: this.link,
5454
- trackedRooms: this.rooms,
5455
- requestOptions: DEFAULT_REQUEST_OPTIONS,
5456
- roomFilter: this.roomFilter,
5457
- onJoined: this.onRoomJoined ?? void 0,
5458
- onError: async (error) => {
5459
- this.logger.warn("RoomPresence failed to subscribe existing rooms", {
5460
- error
5461
- });
5462
- }
5463
- });
5464
- }
5465
- };
5466
-
5467
5490
  // src/runtime/index.ts
5468
- var import_band_sdk_core3 = require("@band-ai/band-sdk-core");
5491
+ var import_band_sdk_core4 = require("@band-ai/band-sdk-core");
5469
5492
 
5470
5493
  // src/runtime/prompts/base.ts
5471
5494
  var ENVIRONMENT_SECTION = `
@@ -5719,6 +5742,7 @@ async function runWithGracefulShutdown(agent, options) {
5719
5742
  PlatformRuntime,
5720
5743
  RetryTracker,
5721
5744
  RoomPresence,
5745
+ RoomRoster,
5722
5746
  SYNTHETIC_CONTACT_EVENTS_SENDER_ID,
5723
5747
  SYNTHETIC_CONTACT_EVENTS_SENDER_NAME,
5724
5748
  SYNTHETIC_SENDER_TYPE,
@@ -5732,6 +5756,7 @@ async function runWithGracefulShutdown(agent, options) {
5732
5756
  getToolDescription,
5733
5757
  isChatEventType,
5734
5758
  mcpToolNames,
5759
+ mentionSubjectsFromMetadata,
5735
5760
  renderSystemPrompt,
5736
5761
  replaceUuidMentions,
5737
5762
  runWithGracefulShutdown