@band-ai/sdk 0.2.0 → 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 +347 -172
  4. package/dist/adapters.d.cts +22 -5
  5. package/dist/adapters.d.ts +22 -5
  6. package/dist/adapters.js +204 -60
  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
@@ -6,9 +6,8 @@ import {
6
6
  buildParticipantsMessage,
7
7
  toParticipantRecord,
8
8
  toParticipantRecordFromRest
9
- } from "./chunk-LBWKISBS.js";
9
+ } from "./chunk-OH6VVVKT.js";
10
10
  import {
11
- CHAT_EVENT_TYPES,
12
11
  CHAT_TOOL_NAMES,
13
12
  MEMORY_LIST_SCOPES,
14
13
  MEMORY_SEGMENTS,
@@ -18,7 +17,6 @@ import {
18
17
  MEMORY_TOOL_NAMES,
19
18
  MEMORY_TYPES,
20
19
  TOOL_MODELS,
21
- assertChatEventType,
22
20
  expectedList,
23
21
  expectedMemoryTypesForSystem,
24
22
  getToolDescription,
@@ -30,7 +28,11 @@ import {
30
28
  isMemoryType,
31
29
  isMemoryTypeForSystem,
32
30
  memoryTypeForSystemError
33
- } from "./chunk-RV76SKFT.js";
31
+ } from "./chunk-MQY74R7D.js";
32
+ import {
33
+ CHAT_EVENT_TYPES,
34
+ assertChatEventType
35
+ } from "./chunk-F63TPOI3.js";
34
36
  import {
35
37
  WebSocketDisconnectError,
36
38
  genericCloseReason,
@@ -41,12 +43,13 @@ import {
41
43
  DEFAULT_REQUEST_OPTIONS,
42
44
  FernRestAdapter,
43
45
  fetchPaginated
44
- } from "./chunk-EAI2M3WM.js";
46
+ } from "./chunk-W5IAI6Z4.js";
45
47
  import {
46
48
  DEFAULT_AGENT_TOOLS_CAPABILITIES,
47
49
  createToolExecutorError,
50
+ isStructuredToolFailure,
48
51
  isToolExecutorError
49
- } from "./chunk-3BKAC4OZ.js";
52
+ } from "./chunk-JADCAMZY.js";
50
53
  import {
51
54
  NoopLogger
52
55
  } from "./chunk-OD2G5LFQ.js";
@@ -1279,10 +1282,12 @@ var AgentTools = class {
1279
1282
  roster;
1280
1283
  adapterTools;
1281
1284
  toolHandlers;
1285
+ logger;
1282
1286
  constructor(options) {
1283
1287
  this.roomId = options.roomId;
1284
1288
  this.rest = options.rest;
1285
1289
  this.roster = options.roster ?? new ParticipantRoster();
1290
+ this.logger = options.logger ?? new NoopLogger();
1286
1291
  this.capabilities = {
1287
1292
  ...DEFAULT_AGENT_TOOLS_CAPABILITIES,
1288
1293
  ...options.capabilities
@@ -1312,14 +1317,23 @@ var AgentTools = class {
1312
1317
  }
1313
1318
  async sendEvent(content, messageType, metadata) {
1314
1319
  assertChatEventType(messageType);
1315
- return this.rest.createChatEvent(
1316
- this.roomId,
1317
- {
1318
- content,
1319
- messageType,
1320
- metadata
1320
+ try {
1321
+ return await this.rest.createChatEvent(
1322
+ this.roomId,
1323
+ {
1324
+ content,
1325
+ messageType,
1326
+ metadata
1327
+ }
1328
+ );
1329
+ } catch (error) {
1330
+ try {
1331
+ this.logger.warn("chat event send failed", { roomId: this.roomId, messageType, error });
1332
+ } catch {
1321
1333
  }
1322
- );
1334
+ const message = error instanceof Error ? error.message : String(error);
1335
+ return { ok: false, status: "failed", message };
1336
+ }
1323
1337
  }
1324
1338
  async createChatroom(taskId) {
1325
1339
  const room = await this.rest.createChat(taskId, DEFAULT_REQUEST_OPTIONS);
@@ -1422,7 +1436,16 @@ var AgentTools = class {
1422
1436
  });
1423
1437
  }
1424
1438
  try {
1425
- return await handler(arguments_);
1439
+ const result = await handler(arguments_);
1440
+ if (toolName === "band_send_event" && isStructuredToolFailure(result) && !isToolExecutorError(result)) {
1441
+ return createToolExecutorError({
1442
+ errorType: "ToolExecutionError",
1443
+ toolName,
1444
+ message: result.message,
1445
+ legacyMessage: `Error executing ${toolName}: ${result.message}`
1446
+ });
1447
+ }
1448
+ return result;
1426
1449
  } catch (error) {
1427
1450
  if (isToolExecutorError(error)) {
1428
1451
  return error;
@@ -2123,7 +2146,8 @@ var ExecutionContext = class {
2123
2146
  roomId: this.roomId,
2124
2147
  rest: this.link.rest,
2125
2148
  roster: this.roster,
2126
- capabilities: this.link.capabilities
2149
+ capabilities: this.link.capabilities,
2150
+ logger: options.logger ?? new NoopLogger()
2127
2151
  });
2128
2152
  this.adapterTools = this.tools.getAdapterTools();
2129
2153
  }
@@ -2371,70 +2395,278 @@ var ExecutionContext = class {
2371
2395
  }
2372
2396
  };
2373
2397
 
2398
+ // src/runtime/rooms/RoomPresence.ts
2399
+ import { RoomRoster } from "@band-ai/band-sdk-core";
2400
+
2374
2401
  // src/runtime/rooms/subscriptions.ts
2375
- function hasRoomId(roomId) {
2376
- return typeof roomId === "string" && roomId.length > 0;
2402
+ async function hydrateExistingRooms(options) {
2403
+ try {
2404
+ const rooms = await options.link.listAllChats(void 0, options.requestOptions);
2405
+ for (const room of rooms) {
2406
+ const roomId = typeof room.id === "string" ? room.id : null;
2407
+ if (!roomId) {
2408
+ continue;
2409
+ }
2410
+ if (options.roomFilter && !options.roomFilter(room)) {
2411
+ continue;
2412
+ }
2413
+ await options.onRoom(roomId, room);
2414
+ }
2415
+ } catch (error) {
2416
+ if (error instanceof UnsupportedFeatureError) {
2417
+ return;
2418
+ }
2419
+ if (options.onError) {
2420
+ await options.onError(error);
2421
+ return;
2422
+ }
2423
+ throw error;
2424
+ }
2377
2425
  }
2378
- async function trackRoomJoin(options) {
2379
- if (!hasRoomId(options.roomId)) {
2380
- return false;
2426
+
2427
+ // src/runtime/rooms/RoomPresence.ts
2428
+ var RoomPresence = class {
2429
+ roster = new RoomRoster();
2430
+ onRoomJoined = null;
2431
+ onRoomLeft = null;
2432
+ onRoomEvent = null;
2433
+ onContactEvent = null;
2434
+ link;
2435
+ roomFilter;
2436
+ autoSubscribeExistingRooms;
2437
+ logger;
2438
+ eventController = null;
2439
+ eventTask = null;
2440
+ contactsSubscribed = false;
2441
+ lifecycle = Promise.resolve();
2442
+ admissionInFlight = /* @__PURE__ */ new Map();
2443
+ // Read once by `admitRoomOrThrow` right after a failed admission; a
2444
+ // subsequent successful subscribe clears it so a caller never attributes
2445
+ // today's failure to a stale reason from an earlier attempt.
2446
+ lastSubscribeError = /* @__PURE__ */ new Map();
2447
+ constructor(options) {
2448
+ this.link = options.link;
2449
+ this.roomFilter = options.roomFilter;
2450
+ this.autoSubscribeExistingRooms = options.autoSubscribeExistingRooms ?? true;
2451
+ this.logger = options.logger ?? new NoopLogger();
2381
2452
  }
2382
- if (options.roomFilter && !options.roomFilter(options.payload)) {
2383
- return false;
2453
+ async start() {
2454
+ return this.serialize(() => this.startBody());
2384
2455
  }
2385
- if (options.trackedRooms.has(options.roomId)) {
2386
- return false;
2456
+ async stop() {
2457
+ return this.serialize(() => this.stopBody());
2387
2458
  }
2388
- await options.link.subscribeRoom(options.roomId);
2389
- options.trackedRooms.add(options.roomId);
2390
- if (options.onJoined) {
2391
- await options.onJoined(options.roomId, options.payload);
2459
+ async [Symbol.asyncDispose]() {
2460
+ await this.stop();
2392
2461
  }
2393
- return true;
2394
- }
2395
- async function trackRoomLeave(options) {
2396
- if (!hasRoomId(options.roomId) || !options.trackedRooms.has(options.roomId)) {
2397
- return false;
2462
+ abortEventLoop() {
2463
+ this.eventController?.abort();
2398
2464
  }
2399
- await options.link.unsubscribeRoom(options.roomId);
2400
- options.trackedRooms.delete(options.roomId);
2401
- if (options.onLeft) {
2402
- await options.onLeft(options.roomId);
2465
+ async waitUntilStopped() {
2466
+ await this.eventTask;
2403
2467
  }
2404
- return true;
2405
- }
2406
- async function hydrateTrackedRooms(options) {
2407
- const pageSize = options.pageSize ?? 100;
2408
- const maxPages = options.maxPages ?? 100;
2409
- try {
2410
- const rooms = await options.link.listAllChats(
2411
- { pageSize, maxPages },
2412
- options.requestOptions
2468
+ async admitRoom(roomId, payload, notify = true) {
2469
+ const ticket = this.roster.beginRoomAdmission(roomId, true);
2470
+ const admitted = ticket === void 0 ? await this.awaitInFlightAdmission(roomId) : await this.performAdmission(roomId, ticket);
2471
+ if (admitted && notify) {
2472
+ await this.onRoomJoined?.(roomId, payload);
2473
+ }
2474
+ return admitted;
2475
+ }
2476
+ /** Admit a room for a caller that needs to know *why* a failed subscribe happened, not just that it did. */
2477
+ async admitRoomOrThrow(roomId) {
2478
+ if (!await this.admitRoom(roomId, {}, false)) {
2479
+ throw new TransportError(`Failed to subscribe to room ${roomId}`, this.lastSubscribeError.get(roomId));
2480
+ }
2481
+ }
2482
+ async awaitInFlightAdmission(roomId) {
2483
+ const inFlight = this.admissionInFlight.get(roomId);
2484
+ return inFlight ? await inFlight : this.roster.roomMembership(roomId) === "admitted";
2485
+ }
2486
+ async performAdmission(roomId, ticket) {
2487
+ const admission = this.completeAdmission(roomId, ticket);
2488
+ this.admissionInFlight.set(roomId, admission);
2489
+ try {
2490
+ return await admission;
2491
+ } finally {
2492
+ if (this.admissionInFlight.get(roomId) === admission) {
2493
+ this.admissionInFlight.delete(roomId);
2494
+ }
2495
+ }
2496
+ }
2497
+ async completeAdmission(roomId, ticket) {
2498
+ let succeeded = false;
2499
+ let admitted = false;
2500
+ let subscribeError;
2501
+ try {
2502
+ await this.link.subscribeRoom(roomId);
2503
+ succeeded = true;
2504
+ } catch (error) {
2505
+ this.logger.warn("RoomPresence failed to subscribe room", { roomId, error });
2506
+ subscribeError = error;
2507
+ } finally {
2508
+ admitted = this.roster.recordRoomAdmission(roomId, ticket, succeeded);
2509
+ }
2510
+ const roomIsAdmitted = admitted || this.roster.roomMembership(roomId) === "admitted";
2511
+ if (succeeded && !admitted && !roomIsAdmitted) {
2512
+ this.logger.debug("RoomPresence admission ticket went stale", { roomId });
2513
+ await this.unsubscribeRoom(roomId);
2514
+ }
2515
+ if (roomIsAdmitted) {
2516
+ this.lastSubscribeError.delete(roomId);
2517
+ } else if (subscribeError !== void 0) {
2518
+ this.lastSubscribeError.set(roomId, subscribeError);
2519
+ }
2520
+ return roomIsAdmitted;
2521
+ }
2522
+ async serialize(body) {
2523
+ const run = this.lifecycle.then(body, body);
2524
+ this.lifecycle = run.then(
2525
+ () => void 0,
2526
+ () => void 0
2413
2527
  );
2414
- for (const room of rooms) {
2415
- await trackRoomJoin({
2416
- link: options.link,
2417
- roomId: typeof room.id === "string" ? room.id : null,
2418
- payload: room,
2419
- trackedRooms: options.trackedRooms,
2420
- roomFilter: options.roomFilter,
2421
- onJoined: options.onJoined
2528
+ return run;
2529
+ }
2530
+ async startBody() {
2531
+ if (this.eventTask) {
2532
+ throw new RuntimeStateError("RoomPresence is already started");
2533
+ }
2534
+ if (!this.link.isConnected()) {
2535
+ await this.link.connect();
2536
+ }
2537
+ const contactsReady = this.subscribeContacts();
2538
+ try {
2539
+ await this.link.subscribeAgentRooms();
2540
+ } catch (error) {
2541
+ this.logger.warn("RoomPresence failed to subscribe agent_rooms channel, continuing without it", {
2542
+ error
2422
2543
  });
2423
2544
  }
2424
- } catch (error) {
2425
- if (error instanceof UnsupportedFeatureError) {
2545
+ if (this.autoSubscribeExistingRooms) {
2546
+ await this.subscribeExistingRooms();
2547
+ }
2548
+ await contactsReady;
2549
+ this.eventController = new AbortController();
2550
+ this.eventTask = this.consumeEvents(this.eventController.signal);
2551
+ }
2552
+ async subscribeContacts() {
2553
+ if (!this.link.capabilities.contacts) {
2426
2554
  return;
2427
2555
  }
2428
- if (options.onError) {
2429
- await options.onError(error);
2556
+ try {
2557
+ await this.link.subscribeAgentContacts();
2558
+ this.contactsSubscribed = true;
2559
+ } catch (error) {
2560
+ this.logger.warn("RoomPresence failed to subscribe agent_contacts channel, continuing without it", {
2561
+ error
2562
+ });
2563
+ }
2564
+ }
2565
+ async stopBody() {
2566
+ this.abortEventLoop();
2567
+ await this.eventTask?.catch(() => void 0);
2568
+ this.eventTask = null;
2569
+ this.eventController = null;
2570
+ if (this.contactsSubscribed) {
2571
+ try {
2572
+ await this.link.unsubscribeAgentContacts();
2573
+ } catch (error) {
2574
+ this.logger.warn("RoomPresence failed to unsubscribe agent_contacts channel", { error });
2575
+ } finally {
2576
+ this.contactsSubscribed = false;
2577
+ }
2578
+ }
2579
+ const roomIds = this.roster.trackedRoomIds();
2580
+ this.roster.clear();
2581
+ this.lastSubscribeError.clear();
2582
+ await Promise.all(
2583
+ roomIds.map(async (roomId) => {
2584
+ await this.unsubscribeRoom(roomId);
2585
+ await this.onRoomLeft?.(roomId);
2586
+ })
2587
+ );
2588
+ }
2589
+ async consumeEvents(signal) {
2590
+ while (!signal.aborted) {
2591
+ const event = await this.link.nextEvent(signal);
2592
+ if (!event) {
2593
+ return;
2594
+ }
2595
+ switch (event.type) {
2596
+ case "room_added":
2597
+ await this.handleRoomAdded(event.roomId, event.payload);
2598
+ break;
2599
+ case "room_removed":
2600
+ case "room_deleted":
2601
+ await this.handleRoomRemoved(event.roomId);
2602
+ break;
2603
+ case "contact_request_received":
2604
+ case "contact_request_updated":
2605
+ case "contact_added":
2606
+ case "contact_removed":
2607
+ await this.onContactEvent?.(event);
2608
+ break;
2609
+ case "message_created":
2610
+ case "participant_added":
2611
+ case "participant_removed":
2612
+ if (event.roomId && this.roster.roomMembership(event.roomId) === "admitted") {
2613
+ await this.onRoomEvent?.(event.roomId, event);
2614
+ }
2615
+ break;
2616
+ default:
2617
+ assertNever(event);
2618
+ }
2619
+ }
2620
+ }
2621
+ async handleRoomAdded(roomId, payload) {
2622
+ if (!roomId) {
2430
2623
  return;
2431
2624
  }
2432
- throw error;
2625
+ if (this.roomFilter && !this.roomFilter(payload)) {
2626
+ return;
2627
+ }
2628
+ await this.admitRoom(roomId, payload);
2629
+ }
2630
+ async handleRoomRemoved(roomId) {
2631
+ if (!roomId) {
2632
+ return;
2633
+ }
2634
+ await this.unsubscribeRoom(roomId);
2635
+ this.lastSubscribeError.delete(roomId);
2636
+ if (!this.roster.recordRoomRemoved(roomId)) {
2637
+ this.logger.debug("RoomPresence ignoring removal for untracked room", { roomId });
2638
+ return;
2639
+ }
2640
+ await this.onRoomLeft?.(roomId);
2641
+ }
2642
+ async unsubscribeRoom(roomId) {
2643
+ try {
2644
+ await this.link.unsubscribeRoom(roomId);
2645
+ } catch (error) {
2646
+ this.logger.warn("RoomPresence failed to unsubscribe room", { roomId, error });
2647
+ }
2648
+ }
2649
+ async subscribeExistingRooms() {
2650
+ await hydrateExistingRooms({
2651
+ link: this.link,
2652
+ roomFilter: this.roomFilter,
2653
+ requestOptions: DEFAULT_REQUEST_OPTIONS,
2654
+ onRoom: async (roomId, payload) => {
2655
+ await this.admitRoom(roomId, payload);
2656
+ },
2657
+ onError: (error) => {
2658
+ this.logger.warn("RoomPresence failed to subscribe existing rooms", { error });
2659
+ }
2660
+ });
2433
2661
  }
2662
+ };
2663
+ function assertNever(value) {
2664
+ throw new Error(`Unhandled platform event: ${JSON.stringify(value)}`);
2434
2665
  }
2435
2666
 
2436
2667
  // src/runtime/rooms/AgentRuntime.ts
2437
2668
  var AgentRuntime = class {
2669
+ presence;
2438
2670
  link;
2439
2671
  agentId;
2440
2672
  onExecute;
@@ -2445,19 +2677,14 @@ var AgentRuntime = class {
2445
2677
  onParticipantAdded;
2446
2678
  onParticipantRemoved;
2447
2679
  onError;
2448
- roomFilter;
2449
2680
  contextFactory;
2450
2681
  sessionConfig;
2451
- autoSubscribeExistingRooms;
2452
- subscribedRooms = /* @__PURE__ */ new Set();
2453
2682
  contexts = /* @__PURE__ */ new Map();
2454
2683
  executions = /* @__PURE__ */ new Map();
2455
2684
  executionWatchers = /* @__PURE__ */ new Map();
2456
2685
  logger;
2457
2686
  running = false;
2458
2687
  stopping = false;
2459
- stopController = new AbortController();
2460
- consumeTask = null;
2461
2688
  fatalError = null;
2462
2689
  constructor(options) {
2463
2690
  this.link = options.link;
@@ -2471,7 +2698,6 @@ var AgentRuntime = class {
2471
2698
  this.onContactEvent = options.onContactEvent;
2472
2699
  this.onParticipantAdded = options.onParticipantAdded;
2473
2700
  this.onParticipantRemoved = options.onParticipantRemoved;
2474
- this.roomFilter = options.roomFilter;
2475
2701
  this.contextFactory = options.contextFactory;
2476
2702
  this.sessionConfig = {
2477
2703
  enableContextCache: options.sessionConfig?.enableContextCache ?? true,
@@ -2480,7 +2706,48 @@ var AgentRuntime = class {
2480
2706
  maxMessageRetries: options.sessionConfig?.maxMessageRetries ?? 1,
2481
2707
  enableContextHydration: options.sessionConfig?.enableContextHydration ?? true
2482
2708
  };
2483
- this.autoSubscribeExistingRooms = options.agentConfig?.autoSubscribeExistingRooms ?? false;
2709
+ this.presence = new RoomPresence({
2710
+ link: this.link,
2711
+ roomFilter: options.roomFilter,
2712
+ autoSubscribeExistingRooms: options.agentConfig?.autoSubscribeExistingRooms ?? false,
2713
+ logger: this.logger
2714
+ });
2715
+ this.presence.onRoomJoined = async (roomId, payload) => {
2716
+ this.getOrCreateExecution(roomId);
2717
+ await this.onRoomJoined?.(roomId, payload);
2718
+ };
2719
+ this.presence.onRoomLeft = async (roomId) => {
2720
+ await this.teardownExecution(roomId);
2721
+ await this.onRoomLeft?.(roomId);
2722
+ };
2723
+ this.presence.onRoomEvent = async (roomId, event) => {
2724
+ switch (event.type) {
2725
+ case "participant_added": {
2726
+ const context = this.getOrCreateContext(roomId);
2727
+ const participant = {
2728
+ id: event.payload.id,
2729
+ name: event.payload.name,
2730
+ type: event.payload.type,
2731
+ handle: event.payload.handle
2732
+ };
2733
+ context.addParticipant(participant);
2734
+ await this.onParticipantAdded?.(roomId, participant);
2735
+ return;
2736
+ }
2737
+ case "participant_removed": {
2738
+ const context = this.getOrCreateContext(roomId);
2739
+ context.removeParticipant(event.payload.id);
2740
+ await this.onParticipantRemoved?.(roomId, event.payload.id);
2741
+ return;
2742
+ }
2743
+ case "message_created":
2744
+ await this.getOrCreateExecution(roomId).enqueue(event);
2745
+ return;
2746
+ default:
2747
+ assertNever2(event);
2748
+ }
2749
+ };
2750
+ this.presence.onContactEvent = this.onContactEvent ?? null;
2484
2751
  }
2485
2752
  async start() {
2486
2753
  if (this.running) {
@@ -2489,45 +2756,17 @@ var AgentRuntime = class {
2489
2756
  this.running = true;
2490
2757
  this.stopping = false;
2491
2758
  this.fatalError = null;
2492
- if (!this.stopController.signal.aborted) {
2493
- this.stopController.abort();
2494
- }
2495
- this.stopController = new AbortController();
2496
2759
  try {
2497
- await this.link.connect();
2760
+ await this.presence.start();
2498
2761
  } catch (error) {
2499
2762
  await this.handleStartFailure();
2500
2763
  throw error;
2501
2764
  }
2502
- try {
2503
- await this.link.subscribeAgentRooms();
2504
- } catch {
2505
- this.logger.warn("AgentRuntime failed to subscribe agent_rooms channel, continuing without it");
2506
- }
2507
- try {
2508
- await this.subscribeExistingRooms();
2509
- } catch (error) {
2510
- await this.handleStartFailure();
2511
- throw error;
2512
- }
2513
- this.consumeTask = this.consumeLoop(this.stopController.signal);
2514
- if (!this.link.capabilities.contacts) {
2515
- return;
2516
- }
2517
- try {
2518
- await this.link.subscribeAgentContacts();
2519
- } catch {
2520
- this.logger.warn("AgentRuntime failed to subscribe agent_contacts channel, continuing without it");
2521
- }
2765
+ void this.presence.waitUntilStopped().catch((error) => this.failRuntime(error, syntheticRuntimeFailureEvent(this.agentId)));
2522
2766
  }
2523
2767
  async handleStartFailure() {
2524
2768
  this.running = false;
2525
2769
  this.stopping = false;
2526
- this.stopController.abort();
2527
- if (this.consumeTask) {
2528
- await this.consumeTask;
2529
- this.consumeTask = null;
2530
- }
2531
2770
  await this.link.disconnect();
2532
2771
  }
2533
2772
  async stop(timeoutMs) {
@@ -2536,34 +2775,25 @@ var AgentRuntime = class {
2536
2775
  }
2537
2776
  this.stopping = true;
2538
2777
  this.running = false;
2539
- this.stopController.abort();
2540
- if (this.consumeTask) {
2541
- await this.consumeTask;
2542
- this.consumeTask = null;
2543
- }
2544
- const deadline = timeoutMs === void 0 ? void 0 : Date.now() + timeoutMs;
2778
+ this.presence.abortEventLoop();
2779
+ await this.presence.waitUntilStopped().catch(() => void 0);
2545
2780
  let graceful = true;
2546
- for (const execution of this.executions.values()) {
2547
- const remaining = deadline === void 0 ? void 0 : Math.max(0, deadline - Date.now());
2548
- const stopped = await execution.stop(remaining);
2549
- if (!stopped) {
2550
- graceful = false;
2551
- }
2552
- }
2553
- for (const roomId of [...this.subscribedRooms]) {
2554
- const remaining = deadline === void 0 ? void 0 : Math.max(0, deadline - Date.now());
2555
- await this.leaveTrackedRoom(roomId, remaining);
2556
- }
2781
+ await Promise.all(
2782
+ [...this.executions].map(async ([roomId, execution]) => {
2783
+ const stopped = await execution.stop(timeoutMs);
2784
+ if (!stopped) {
2785
+ graceful = false;
2786
+ }
2787
+ this.executions.delete(roomId);
2788
+ })
2789
+ );
2790
+ await this.presence.stop();
2557
2791
  for (const roomId of [...this.contexts.keys()]) {
2558
2792
  await this.onSessionCleanup(roomId);
2559
2793
  }
2560
- this.subscribedRooms.clear();
2561
2794
  this.contexts.clear();
2562
2795
  this.executions.clear();
2563
2796
  this.executionWatchers.clear();
2564
- if (this.link.capabilities.contacts) {
2565
- await this.link.unsubscribeAgentContacts();
2566
- }
2567
2797
  await this.link.disconnect();
2568
2798
  if (this.fatalError) {
2569
2799
  throw this.fatalError instanceof Error ? this.fatalError : new Error(String(this.fatalError));
@@ -2574,11 +2804,7 @@ var AgentRuntime = class {
2574
2804
  return this.contexts.get(roomId);
2575
2805
  }
2576
2806
  async waitUntilStopped() {
2577
- if (this.consumeTask) {
2578
- await this.consumeTask;
2579
- } else {
2580
- await this.link.runForever(this.stopController.signal);
2581
- }
2807
+ await this.presence.waitUntilStopped().catch(() => void 0);
2582
2808
  if (this.fatalError) {
2583
2809
  throw this.fatalError instanceof Error ? this.fatalError : new Error(String(this.fatalError));
2584
2810
  }
@@ -2586,97 +2812,19 @@ var AgentRuntime = class {
2586
2812
  getContexts() {
2587
2813
  return [...this.contexts.values()];
2588
2814
  }
2589
- async consumeLoop(signal) {
2590
- while (!signal.aborted) {
2591
- let event;
2592
- try {
2593
- event = await this.link.nextEvent(signal);
2594
- } catch (error) {
2595
- await this.failRuntime(error, syntheticRuntimeFailureEvent(this.agentId));
2596
- return;
2597
- }
2598
- if (!event) {
2599
- return;
2600
- }
2601
- try {
2602
- await this.handleEvent(event);
2603
- } catch (error) {
2604
- await this.failRuntime(error, event);
2605
- return;
2606
- }
2607
- }
2608
- }
2609
- async handleEvent(event) {
2610
- switch (event.type) {
2611
- case "room_added":
2612
- await trackRoomJoin({
2613
- link: this.link,
2614
- roomId: event.roomId,
2615
- payload: event.payload,
2616
- trackedRooms: this.subscribedRooms,
2617
- roomFilter: this.roomFilter,
2618
- onJoined: async (roomId) => {
2619
- this.getOrCreateExecution(roomId);
2620
- await this.onRoomJoined?.(roomId, event.payload);
2621
- }
2622
- });
2623
- return;
2624
- case "room_removed":
2625
- case "room_deleted":
2626
- if (event.roomId) {
2627
- await this.onRoomLeft?.(event.roomId);
2628
- await this.leaveTrackedRoom(event.roomId);
2629
- }
2630
- return;
2631
- case "participant_added":
2632
- if (event.roomId) {
2633
- const context = this.getOrCreateContext(event.roomId);
2634
- const participant = {
2635
- id: event.payload.id,
2636
- name: event.payload.name,
2637
- type: event.payload.type,
2638
- handle: event.payload.handle
2639
- };
2640
- context.addParticipant(participant);
2641
- await this.onParticipantAdded?.(event.roomId, participant);
2642
- }
2643
- return;
2644
- case "participant_removed":
2645
- if (event.roomId) {
2646
- const context = this.getOrCreateContext(event.roomId);
2647
- context.removeParticipant(event.payload.id);
2648
- await this.onParticipantRemoved?.(event.roomId, event.payload.id);
2649
- }
2650
- return;
2651
- case "contact_request_received":
2652
- case "contact_request_updated":
2653
- case "contact_added":
2654
- case "contact_removed":
2655
- await this.onContactEvent?.(event);
2656
- return;
2657
- case "message_created":
2658
- if (!event.roomId) {
2659
- return;
2660
- }
2661
- await this.getOrCreateExecution(event.roomId).enqueue(event);
2662
- return;
2663
- }
2664
- return assertNever(event);
2665
- }
2666
2815
  async enqueueEvent(roomId, event) {
2667
2816
  await this.getOrCreateExecution(roomId).enqueue(event);
2668
2817
  }
2669
2818
  async bootstrapRoomMessage(roomId, message) {
2670
- await this.link.subscribeRoom(roomId);
2671
- this.subscribedRooms.add(roomId);
2819
+ await this.presence.admitRoomOrThrow(roomId);
2672
2820
  await this.getOrCreateExecution(roomId).bootstrapMessage(message);
2673
2821
  }
2674
2822
  async resetRoomSession(roomId, timeoutMs) {
2823
+ return this.teardownExecution(roomId, timeoutMs);
2824
+ }
2825
+ async teardownExecution(roomId, timeoutMs) {
2675
2826
  const execution = this.executions.get(roomId);
2676
- let graceful = true;
2677
- if (execution) {
2678
- graceful = await execution.stop(timeoutMs);
2679
- }
2827
+ const graceful = execution ? await execution.stop(timeoutMs) : true;
2680
2828
  this.executions.delete(roomId);
2681
2829
  this.contexts.delete(roomId);
2682
2830
  await this.onSessionCleanup(roomId);
@@ -2732,44 +2880,13 @@ var AgentRuntime = class {
2732
2880
  maxMessageRetries: this.sessionConfig.maxMessageRetries,
2733
2881
  enableContextCache: this.sessionConfig.enableContextCache,
2734
2882
  contextCacheTtlSeconds: this.sessionConfig.contextCacheTtlSeconds,
2735
- enableContextHydration: this.sessionConfig.enableContextHydration
2883
+ enableContextHydration: this.sessionConfig.enableContextHydration,
2884
+ logger: this.logger
2736
2885
  };
2737
2886
  const context = this.contextFactory ? this.contextFactory(roomId, defaults) : new ExecutionContext(defaults);
2738
2887
  this.contexts.set(roomId, context);
2739
2888
  return context;
2740
2889
  }
2741
- async subscribeExistingRooms() {
2742
- if (!this.autoSubscribeExistingRooms) {
2743
- return;
2744
- }
2745
- await hydrateTrackedRooms({
2746
- link: this.link,
2747
- trackedRooms: this.subscribedRooms,
2748
- roomFilter: this.roomFilter,
2749
- onJoined: async (roomId, payload) => {
2750
- this.getOrCreateExecution(roomId);
2751
- await this.onRoomJoined?.(roomId, payload);
2752
- },
2753
- onError: async (error) => {
2754
- this.logger.warn("AgentRuntime failed to subscribe existing rooms", {
2755
- error
2756
- });
2757
- }
2758
- });
2759
- }
2760
- async leaveTrackedRoom(roomId, timeoutMs) {
2761
- await trackRoomLeave({
2762
- link: this.link,
2763
- roomId,
2764
- trackedRooms: this.subscribedRooms,
2765
- onLeft: async (leftRoomId) => {
2766
- await this.executions.get(leftRoomId)?.stop(timeoutMs);
2767
- this.contexts.delete(leftRoomId);
2768
- this.executions.delete(leftRoomId);
2769
- await this.onSessionCleanup(leftRoomId);
2770
- }
2771
- });
2772
- }
2773
2890
  async failRuntime(error, event) {
2774
2891
  if (!this.fatalError) {
2775
2892
  this.fatalError = error;
@@ -2781,9 +2898,7 @@ var AgentRuntime = class {
2781
2898
  });
2782
2899
  this.notifyOnError(error, event);
2783
2900
  }
2784
- if (!this.stopController.signal.aborted) {
2785
- this.stopController.abort();
2786
- }
2901
+ this.presence.abortEventLoop();
2787
2902
  }
2788
2903
  notifyOnError(error, event) {
2789
2904
  if (!this.onError) {
@@ -2800,6 +2915,9 @@ var AgentRuntime = class {
2800
2915
  }
2801
2916
  }
2802
2917
  };
2918
+ function assertNever2(value) {
2919
+ throw new Error(`Unhandled room event: ${JSON.stringify(value)}`);
2920
+ }
2803
2921
  function syntheticRuntimeFailureEvent(agentId) {
2804
2922
  return {
2805
2923
  type: "message_created",
@@ -2817,9 +2935,6 @@ function syntheticRuntimeFailureEvent(agentId) {
2817
2935
  }
2818
2936
  };
2819
2937
  }
2820
- function assertNever(value) {
2821
- throw new Error(`Unhandled platform event: ${JSON.stringify(value)}`);
2822
- }
2823
2938
 
2824
2939
  // src/runtime/preprocessing/DefaultPreprocessor.ts
2825
2940
  function toPlatformMessage2(roomId, payload) {
@@ -3417,7 +3532,7 @@ Type: ${event.payload.type}, ID: ${event.payload.id}`;
3417
3532
  case "contact_removed":
3418
3533
  return `[Contact Removed] Contact ${event.payload.id} was removed.`;
3419
3534
  }
3420
- return assertNever2(event);
3535
+ return assertNever3(event);
3421
3536
  }
3422
3537
  formatBroadcast(event) {
3423
3538
  switch (event.type) {
@@ -3547,7 +3662,7 @@ Type: ${event.payload.type}, ID: ${event.payload.id}`;
3547
3662
  return { message: String(error) };
3548
3663
  }
3549
3664
  };
3550
- function assertNever2(value) {
3665
+ function assertNever3(value) {
3551
3666
  throw new Error(`Unhandled contact event: ${JSON.stringify(value)}`);
3552
3667
  }
3553
3668
  function normalizeHandle(handle) {
@@ -3764,6 +3879,9 @@ var PlatformRuntime = class {
3764
3879
  this.stopping = false;
3765
3880
  return graceful;
3766
3881
  }
3882
+ async [Symbol.asyncDispose]() {
3883
+ await this.stop();
3884
+ }
3767
3885
  async runForever() {
3768
3886
  if (!this.runtime) {
3769
3887
  throw new RuntimeStateError("Runtime not started");
@@ -3880,9 +3998,7 @@ export {
3880
3998
  Execution,
3881
3999
  AgentTools,
3882
4000
  ExecutionContext,
3883
- trackRoomJoin,
3884
- trackRoomLeave,
3885
- hydrateTrackedRooms,
4001
+ RoomPresence,
3886
4002
  AgentRuntime,
3887
4003
  DefaultPreprocessor,
3888
4004
  ContactToolsImpl,