@ekodb/ekodb-client 0.11.0 → 0.13.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/dist/client.js CHANGED
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.WebSocketClient = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
39
+ exports.WebSocketClient = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
40
40
  const msgpack_1 = require("@msgpack/msgpack");
41
41
  const query_builder_1 = require("./query-builder");
42
42
  const schema_1 = require("./schema");
@@ -66,6 +66,7 @@ var MergeStrategy;
66
66
  MergeStrategy["Chronological"] = "Chronological";
67
67
  MergeStrategy["Summarized"] = "Summarized";
68
68
  MergeStrategy["LatestOnly"] = "LatestOnly";
69
+ MergeStrategy["Interleaved"] = "Interleaved";
69
70
  })(MergeStrategy || (exports.MergeStrategy = MergeStrategy = {}));
70
71
  class EkoDBClient {
71
72
  constructor(config, apiKey) {
@@ -77,7 +78,6 @@ class EkoDBClient {
77
78
  this.apiKey = apiKey;
78
79
  this.shouldRetry = true;
79
80
  this.maxRetries = 3;
80
- this.timeout = 30000;
81
81
  this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
82
82
  }
83
83
  else {
@@ -85,7 +85,6 @@ class EkoDBClient {
85
85
  this.apiKey = config.apiKey;
86
86
  this.shouldRetry = config.shouldRetry ?? true;
87
87
  this.maxRetries = config.maxRetries ?? 3;
88
- this.timeout = config.timeout ?? 30000;
89
88
  this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
90
89
  }
91
90
  }
@@ -135,6 +134,20 @@ class EkoDBClient {
135
134
  const result = (await response.json());
136
135
  this.token = result.token;
137
136
  }
137
+ /**
138
+ * Get the current authentication token.
139
+ * Returns null if not yet authenticated. Call refreshToken() first.
140
+ */
141
+ getToken() {
142
+ return this.token;
143
+ }
144
+ /**
145
+ * Clear the cached authentication token.
146
+ * The next request will trigger a fresh token exchange.
147
+ */
148
+ clearTokenCache() {
149
+ this.token = null;
150
+ }
138
151
  /**
139
152
  * Extract rate limit information from response headers
140
153
  */
@@ -325,6 +338,26 @@ class EkoDBClient {
325
338
  async findById(collection, id) {
326
339
  return this.makeRequest("GET", `/api/find/${collection}/${id}`);
327
340
  }
341
+ /**
342
+ * Find a document by ID with field projection
343
+ * @param collection - Collection name
344
+ * @param id - Document ID
345
+ * @param selectFields - Fields to include in the result
346
+ * @param excludeFields - Fields to exclude from the result
347
+ */
348
+ async findByIdWithProjection(collection, id, selectFields, excludeFields) {
349
+ const params = new URLSearchParams();
350
+ if (selectFields?.length) {
351
+ params.append("select_fields", selectFields.join(","));
352
+ }
353
+ if (excludeFields?.length) {
354
+ params.append("exclude_fields", excludeFields.join(","));
355
+ }
356
+ const url = params.toString()
357
+ ? `/api/find/${collection}/${id}?${params.toString()}`
358
+ : `/api/find/${collection}/${id}`;
359
+ return this.makeRequest("GET", url);
360
+ }
328
361
  /**
329
362
  * Update a document
330
363
  * @param collection - Collection name
@@ -345,6 +378,40 @@ class EkoDBClient {
345
378
  : `/api/update/${collection}/${id}`;
346
379
  return this.makeRequest("PUT", url, record);
347
380
  }
381
+ /**
382
+ * Apply an atomic field action to a single field of a record.
383
+ *
384
+ * Use this instead of `update()` for safe concurrent modifications like
385
+ * incrementing counters, pushing to arrays, or arithmetic operations.
386
+ *
387
+ * @param collection - Collection name
388
+ * @param id - Record ID
389
+ * @param action - The atomic action: increment, decrement, multiply, divide, modulo,
390
+ * push, pop, shift, unshift, remove, append, clear
391
+ * @param field - The field name to apply the action to
392
+ * @param value - The value for the action (omit for pop/shift/clear)
393
+ */
394
+ async updateWithAction(collection, id, action, field, value) {
395
+ const url = `/api/update/${collection}/${id}/action/${action}`;
396
+ return this.makeRequest("PUT", url, {
397
+ field,
398
+ value: value ?? null,
399
+ });
400
+ }
401
+ /**
402
+ * Apply a sequence of atomic field actions to a record in a single request.
403
+ *
404
+ * All actions are applied atomically — the record is fetched once, all actions
405
+ * run in order, and the result is persisted in a single update.
406
+ *
407
+ * @param collection - Collection name
408
+ * @param id - Record ID
409
+ * @param actions - Array of [action, field, value] tuples
410
+ */
411
+ async updateWithActionSequence(collection, id, actions) {
412
+ const url = `/api/update/sequence/${collection}/${id}`;
413
+ return this.makeRequest("PUT", url, actions);
414
+ }
348
415
  /**
349
416
  * Delete a document
350
417
  * @param collection - Collection name
@@ -771,6 +838,36 @@ class EkoDBClient {
771
838
  // Ensure all parameters from SearchQuery are sent to server
772
839
  return this.makeRequest("POST", `/api/search/${collection}`, query, 0, true);
773
840
  }
841
+ /**
842
+ * Get distinct (unique) values for a field across all records in a collection.
843
+ *
844
+ * Results are deduplicated and sorted alphabetically. Supports an optional filter
845
+ * to restrict which records are examined.
846
+ *
847
+ * @param collection - Collection name
848
+ * @param field - Field to get distinct values for
849
+ * @param options - Optional filter and bypass flags
850
+ *
851
+ * @example
852
+ * // All distinct statuses
853
+ * const resp = await client.distinctValues("orders", "status");
854
+ * console.log(resp.values); // ["active", "cancelled", "shipped"]
855
+ *
856
+ * // Only statuses for US orders
857
+ * const resp = await client.distinctValues("orders", "status", {
858
+ * filter: { type: "Condition", content: { field: "region", operator: "Eq", value: "us" } }
859
+ * });
860
+ */
861
+ async distinctValues(collection, field, options = {}) {
862
+ const body = {};
863
+ if (options.filter !== undefined)
864
+ body.filter = options.filter;
865
+ if (options.bypassRipple !== undefined)
866
+ body.bypass_ripple = options.bypassRipple;
867
+ if (options.bypassCache !== undefined)
868
+ body.bypass_cache = options.bypassCache;
869
+ return this.makeRequest("POST", `/api/distinct/${collection}/${field}`, body, 0, true);
870
+ }
774
871
  /**
775
872
  * Health check - verify the ekoDB server is responding
776
873
  */
@@ -790,6 +887,25 @@ class EkoDBClient {
790
887
  async createChatSession(request) {
791
888
  return this.makeRequest("POST", "/api/chat", request, 0, true);
792
889
  }
890
+ /**
891
+ * Stateless raw LLM completion — no session, no history, no RAG.
892
+ *
893
+ * Sends a system prompt and user message directly to the LLM via ekoDB
894
+ * and returns the raw text response without any context injection or
895
+ * conversation management. Use this for structured-output tasks such as
896
+ * planning where the response must be parsed programmatically.
897
+ *
898
+ * @example
899
+ * const resp = await client.rawCompletion({
900
+ * system_prompt: "You are a helpful assistant.",
901
+ * message: "Summarize this in JSON.",
902
+ * max_tokens: 2048,
903
+ * });
904
+ * console.log(resp.content);
905
+ */
906
+ async rawCompletion(request) {
907
+ return this.makeRequest("POST", "/api/chat/complete", request, 0, true);
908
+ }
793
909
  /**
794
910
  * Send a message in an existing chat session
795
911
  */
@@ -889,6 +1005,14 @@ class EkoDBClient {
889
1005
  async getChatModels() {
890
1006
  return this.makeRequest("GET", "/api/chat_models", undefined, 0, true);
891
1007
  }
1008
+ /**
1009
+ * Get all built-in server-side chat tool definitions.
1010
+ * Returns a list of tool objects with name, description, and parameters fields.
1011
+ * Used by planning agents to discover available tools dynamically.
1012
+ */
1013
+ async getChatTools() {
1014
+ return this.makeRequest("GET", "/api/chat/tools", undefined, 0, true);
1015
+ }
892
1016
  /**
893
1017
  * Get available models for a specific provider
894
1018
  * @param provider - Provider name (e.g., "openai", "anthropic", "perplexity")
@@ -1026,13 +1150,7 @@ class EkoDBClient {
1026
1150
  }
1027
1151
  // ========== RAG Helper Methods ==========
1028
1152
  /**
1029
- * Generate embeddings for text using ekoDB's native Functions
1030
- *
1031
- * This helper simplifies embedding generation by:
1032
- * 1. Creating a temporary collection with the text
1033
- * 2. Running a Script with FindAll + Embed Functions
1034
- * 3. Extracting and returning the embedding vector
1035
- * 4. Cleaning up temporary resources
1153
+ * Generate embeddings for a single text
1036
1154
  *
1037
1155
  * @param text - The text to generate embeddings for
1038
1156
  * @param model - The embedding model to use (e.g., "text-embedding-3-small")
@@ -1048,52 +1166,28 @@ class EkoDBClient {
1048
1166
  * ```
1049
1167
  */
1050
1168
  async embed(text, model) {
1051
- const tempCollection = `embed_temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1052
- try {
1053
- // Insert temporary record with the text
1054
- await this.insert(tempCollection, { text }, undefined);
1055
- // Create Script with FindAll + Embed Functions
1056
- const tempLabel = `embed_script_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1057
- const script = {
1058
- label: tempLabel,
1059
- name: "Generate Embedding",
1060
- description: "Temporary script for embedding generation",
1061
- version: "1.0",
1062
- parameters: {},
1063
- functions: [
1064
- {
1065
- type: "FindAll",
1066
- collection: tempCollection,
1067
- },
1068
- {
1069
- type: "Embed",
1070
- input_field: "text",
1071
- output_field: "embedding",
1072
- model: model,
1073
- },
1074
- ],
1075
- tags: [],
1076
- };
1077
- // Save and execute the script
1078
- const scriptId = await this.saveScript(script);
1079
- const result = await this.callScript(scriptId, undefined);
1080
- // Clean up
1081
- await this.deleteScript(scriptId).catch(() => { });
1082
- await this.deleteCollection(tempCollection).catch(() => { });
1083
- // Extract embedding from result
1084
- if (result.records && result.records.length > 0) {
1085
- const record = result.records[0];
1086
- if (record.embedding && Array.isArray(record.embedding)) {
1087
- return record.embedding;
1088
- }
1089
- }
1090
- throw new Error("Failed to extract embedding from result");
1091
- }
1092
- catch (error) {
1093
- // Ensure cleanup even on error
1094
- await this.deleteCollection(tempCollection).catch(() => { });
1095
- throw error;
1169
+ const response = await this.embedRequest({ text, model });
1170
+ if (response.embeddings.length === 0) {
1171
+ throw new Error("No embedding returned");
1096
1172
  }
1173
+ return response.embeddings[0];
1174
+ }
1175
+ /**
1176
+ * Generate embeddings for multiple texts in a single batch request
1177
+ *
1178
+ * @param texts - Array of texts to generate embeddings for
1179
+ * @param model - The embedding model to use
1180
+ * @returns Array of embedding vectors
1181
+ */
1182
+ async embedBatch(texts, model) {
1183
+ const response = await this.embedRequest({ texts, model });
1184
+ return response.embeddings;
1185
+ }
1186
+ /**
1187
+ * Internal: make embed API request
1188
+ */
1189
+ async embedRequest(request) {
1190
+ return this.makeRequest("POST", "/api/embed", request, 0, true);
1097
1191
  }
1098
1192
  /**
1099
1193
  * Perform text search without embeddings
@@ -1179,22 +1273,64 @@ class EkoDBClient {
1179
1273
  }
1180
1274
  }
1181
1275
  exports.EkoDBClient = EkoDBClient;
1276
+ /** EventEmitter-like interface for subscriptions and chat streams. */
1277
+ class EventStream {
1278
+ constructor() {
1279
+ this.listeners = new Map();
1280
+ this._closed = false;
1281
+ }
1282
+ on(event, listener) {
1283
+ if (!this.listeners.has(event)) {
1284
+ this.listeners.set(event, []);
1285
+ }
1286
+ this.listeners.get(event).push(listener);
1287
+ return this;
1288
+ }
1289
+ /** @internal */
1290
+ emit(event, data) {
1291
+ const handlers = this.listeners.get(event);
1292
+ if (handlers) {
1293
+ for (const handler of handlers) {
1294
+ handler(data);
1295
+ }
1296
+ }
1297
+ }
1298
+ get closed() {
1299
+ return this._closed;
1300
+ }
1301
+ /** @internal */
1302
+ close() {
1303
+ this._closed = true;
1304
+ this.emit("close");
1305
+ }
1306
+ }
1307
+ exports.EventStream = EventStream;
1182
1308
  /**
1183
- * WebSocket client for real-time queries
1309
+ * WebSocket client for real-time queries, subscriptions, and chat streaming.
1184
1310
  */
1185
1311
  class WebSocketClient {
1186
1312
  constructor(wsURL, token) {
1187
1313
  this.ws = null;
1314
+ this.dispatcherRunning = false;
1315
+ // Dispatcher state
1316
+ this.pendingRequests = new Map();
1317
+ this.subscriptions = new Map();
1318
+ this.chatStreams = new Map();
1319
+ this.registerToolsAck = null;
1320
+ this.messageCounter = 0;
1188
1321
  this.wsURL = wsURL;
1189
1322
  this.token = token;
1190
1323
  }
1324
+ genMessageId() {
1325
+ const counter = this.messageCounter++;
1326
+ return `${Date.now()}-${counter}-${Math.random().toString(36).slice(2, 8)}`;
1327
+ }
1191
1328
  /**
1192
- * Connect to WebSocket
1329
+ * Connect and start the dispatcher.
1193
1330
  */
1194
- async connect() {
1195
- if (this.ws)
1331
+ async ensureConnected() {
1332
+ if (this.ws && this.dispatcherRunning)
1196
1333
  return;
1197
- // Dynamic import for Node.js WebSocket
1198
1334
  const WebSocket = (await Promise.resolve().then(() => __importStar(require("ws")))).default;
1199
1335
  let url = this.wsURL;
1200
1336
  if (!url.endsWith("/api/ws")) {
@@ -1205,43 +1341,277 @@ class WebSocketClient {
1205
1341
  Authorization: `Bearer ${this.token}`,
1206
1342
  },
1207
1343
  });
1208
- return new Promise((resolve, reject) => {
1344
+ await new Promise((resolve, reject) => {
1209
1345
  this.ws.on("open", () => resolve());
1210
1346
  this.ws.on("error", (err) => reject(err));
1211
1347
  });
1348
+ this.spawnDispatcher();
1349
+ }
1350
+ spawnDispatcher() {
1351
+ if (this.dispatcherRunning)
1352
+ return;
1353
+ this.dispatcherRunning = true;
1354
+ this.ws.on("message", (data) => {
1355
+ try {
1356
+ const msg = JSON.parse(data.toString());
1357
+ this.routeMessage(msg);
1358
+ }
1359
+ catch {
1360
+ // Ignore malformed messages
1361
+ }
1362
+ });
1363
+ this.ws.on("close", () => {
1364
+ this.dispatcherRunning = false;
1365
+ // Notify all pending requests
1366
+ for (const [, pending] of this.pendingRequests) {
1367
+ pending.reject(new Error("WebSocket connection closed"));
1368
+ }
1369
+ this.pendingRequests.clear();
1370
+ // Close all chat streams
1371
+ for (const [, stream] of this.chatStreams) {
1372
+ stream.emit("event", { type: "error", error: "Connection closed" });
1373
+ stream.close();
1374
+ }
1375
+ this.chatStreams.clear();
1376
+ // Close all subscriptions
1377
+ for (const [, stream] of this.subscriptions) {
1378
+ stream.close();
1379
+ }
1380
+ this.subscriptions.clear();
1381
+ this.ws = null;
1382
+ });
1383
+ }
1384
+ routeMessage(msg) {
1385
+ switch (msg.type) {
1386
+ case "Success":
1387
+ case "Error": {
1388
+ const messageId = msg.payload?.message_id || msg.payload?.messageId;
1389
+ if (messageId && this.pendingRequests.has(messageId)) {
1390
+ const pending = this.pendingRequests.get(messageId);
1391
+ this.pendingRequests.delete(messageId);
1392
+ if (msg.type === "Error") {
1393
+ pending.reject(new Error(msg.message || "Unknown error"));
1394
+ }
1395
+ else {
1396
+ pending.resolve(msg.payload);
1397
+ }
1398
+ }
1399
+ else if (this.registerToolsAck) {
1400
+ const ack = this.registerToolsAck;
1401
+ this.registerToolsAck = null;
1402
+ if (msg.type === "Error") {
1403
+ ack.reject(new Error(msg.message || "Tool registration failed"));
1404
+ }
1405
+ else {
1406
+ ack.resolve(msg.payload);
1407
+ }
1408
+ }
1409
+ break;
1410
+ }
1411
+ case "MutationNotification": {
1412
+ const payload = msg.payload;
1413
+ const notification = {
1414
+ collection: payload.collection,
1415
+ event: payload.event,
1416
+ recordIds: payload.record_ids || payload.recordIds || [],
1417
+ records: payload.records,
1418
+ timestamp: payload.timestamp,
1419
+ };
1420
+ for (const [collection, stream] of this.subscriptions) {
1421
+ if (collection === notification.collection) {
1422
+ stream.emit("mutation", notification);
1423
+ }
1424
+ }
1425
+ break;
1426
+ }
1427
+ case "ChatStreamChunk": {
1428
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1429
+ const stream = this.chatStreams.get(chatId);
1430
+ if (stream) {
1431
+ stream.emit("event", {
1432
+ type: "chunk",
1433
+ content: msg.payload.content,
1434
+ });
1435
+ }
1436
+ break;
1437
+ }
1438
+ case "ChatStreamEnd": {
1439
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1440
+ const stream = this.chatStreams.get(chatId);
1441
+ if (stream) {
1442
+ stream.emit("event", {
1443
+ type: "end",
1444
+ messageId: msg.payload.message_id || msg.payload.messageId || "",
1445
+ tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
1446
+ toolCallHistory: msg.payload.tool_call_history || msg.payload.toolCallHistory,
1447
+ executionTimeMs: msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
1448
+ });
1449
+ this.chatStreams.delete(chatId);
1450
+ stream.close();
1451
+ }
1452
+ break;
1453
+ }
1454
+ case "ChatStreamError": {
1455
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1456
+ const stream = this.chatStreams.get(chatId);
1457
+ if (stream) {
1458
+ stream.emit("event", {
1459
+ type: "error",
1460
+ error: msg.payload.error || msg.payload.message || "Unknown error",
1461
+ });
1462
+ this.chatStreams.delete(chatId);
1463
+ stream.close();
1464
+ }
1465
+ break;
1466
+ }
1467
+ case "ClientToolCall": {
1468
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1469
+ const stream = this.chatStreams.get(chatId);
1470
+ if (stream) {
1471
+ stream.emit("event", {
1472
+ type: "toolCall",
1473
+ chatId,
1474
+ callId: msg.payload.call_id || msg.payload.callId,
1475
+ toolName: msg.payload.tool_name || msg.payload.toolName,
1476
+ arguments: msg.payload.arguments,
1477
+ });
1478
+ }
1479
+ break;
1480
+ }
1481
+ }
1482
+ }
1483
+ async sendRequest(request) {
1484
+ await this.ensureConnected();
1485
+ const messageId = request.messageId || request.message_id;
1486
+ return new Promise((resolve, reject) => {
1487
+ this.pendingRequests.set(messageId, { resolve, reject });
1488
+ try {
1489
+ this.ws.send(JSON.stringify(request));
1490
+ }
1491
+ catch (err) {
1492
+ this.pendingRequests.delete(messageId);
1493
+ reject(err);
1494
+ }
1495
+ });
1212
1496
  }
1213
1497
  /**
1214
- * Find all records in a collection via WebSocket
1498
+ * Find all records in a collection via WebSocket.
1215
1499
  */
1216
1500
  async findAll(collection) {
1217
- await this.connect();
1218
- const messageId = Date.now().toString();
1219
- const request = {
1501
+ const messageId = this.genMessageId();
1502
+ const payload = await this.sendRequest({
1220
1503
  type: "FindAll",
1221
1504
  messageId,
1222
1505
  payload: { collection },
1506
+ });
1507
+ return payload?.data || [];
1508
+ }
1509
+ /**
1510
+ * Subscribe to mutation notifications on a collection.
1511
+ * Returns an EventStream that emits "mutation" events.
1512
+ */
1513
+ async subscribe(collection, options) {
1514
+ await this.ensureConnected();
1515
+ if (this.subscriptions.has(collection)) {
1516
+ throw new Error(`Already subscribed to collection "${collection}"`);
1517
+ }
1518
+ const messageId = this.genMessageId();
1519
+ const stream = new EventStream();
1520
+ this.subscriptions.set(collection, stream);
1521
+ const request = {
1522
+ type: "Subscribe",
1523
+ messageId,
1524
+ payload: {
1525
+ collection,
1526
+ ...(options?.filterField && { filter_field: options.filterField }),
1527
+ ...(options?.filterValue && { filter_value: options.filterValue }),
1528
+ },
1223
1529
  };
1224
- return new Promise((resolve, reject) => {
1530
+ // Send subscribe request and wait for ack
1531
+ try {
1532
+ await this.sendRequest(request);
1533
+ }
1534
+ catch (err) {
1535
+ this.subscriptions.delete(collection);
1536
+ throw err;
1537
+ }
1538
+ return stream;
1539
+ }
1540
+ /**
1541
+ * Send a chat message and receive a streaming response.
1542
+ * Returns an EventStream that emits "event" with ChatStreamEvent objects.
1543
+ */
1544
+ async chatSend(chatId, message, options) {
1545
+ await this.ensureConnected();
1546
+ if (this.chatStreams.has(chatId)) {
1547
+ throw new Error(`Chat stream already active for chatId "${chatId}"`);
1548
+ }
1549
+ const stream = new EventStream();
1550
+ this.chatStreams.set(chatId, stream);
1551
+ const request = {
1552
+ type: "ChatSend",
1553
+ payload: {
1554
+ chat_id: chatId,
1555
+ message,
1556
+ ...(options?.bypassRipple != null && {
1557
+ bypass_ripple: options.bypassRipple,
1558
+ }),
1559
+ ...(options?.clientTools && { client_tools: options.clientTools }),
1560
+ ...(options?.maxIterations != null && {
1561
+ max_iterations: options.maxIterations,
1562
+ }),
1563
+ ...(options?.confirmTools && { confirm_tools: options.confirmTools }),
1564
+ ...(options?.excludeTools && { exclude_tools: options.excludeTools }),
1565
+ },
1566
+ };
1567
+ this.ws.send(JSON.stringify(request));
1568
+ return stream;
1569
+ }
1570
+ /**
1571
+ * Register client-side tools for a chat session.
1572
+ */
1573
+ async registerClientTools(chatId, tools) {
1574
+ await this.ensureConnected();
1575
+ const request = {
1576
+ type: "RegisterClientTools",
1577
+ payload: {
1578
+ chat_id: chatId,
1579
+ tools,
1580
+ },
1581
+ };
1582
+ await new Promise((resolve, reject) => {
1583
+ this.registerToolsAck = {
1584
+ resolve: () => resolve(),
1585
+ reject: (err) => reject(err),
1586
+ };
1225
1587
  this.ws.send(JSON.stringify(request));
1226
- this.ws.once("message", (data) => {
1227
- const response = JSON.parse(data.toString());
1228
- if (response.type === "Error") {
1229
- reject(new Error(response.message));
1230
- }
1231
- else {
1232
- resolve(response.payload?.data || []);
1233
- }
1234
- });
1235
- this.ws.once("error", reject);
1236
1588
  });
1237
1589
  }
1238
1590
  /**
1239
- * Close the WebSocket connection
1591
+ * Send a tool result back to the server during a chat stream.
1592
+ */
1593
+ async sendToolResult(chatId, callId, success, result, error) {
1594
+ await this.ensureConnected();
1595
+ const request = {
1596
+ type: "ClientToolResult",
1597
+ payload: {
1598
+ chat_id: chatId,
1599
+ call_id: callId,
1600
+ success,
1601
+ ...(result !== undefined && { result }),
1602
+ ...(error !== undefined && { error }),
1603
+ },
1604
+ };
1605
+ this.ws.send(JSON.stringify(request));
1606
+ }
1607
+ /**
1608
+ * Close the WebSocket connection.
1240
1609
  */
1241
1610
  close() {
1242
1611
  if (this.ws) {
1243
1612
  this.ws.close();
1244
1613
  this.ws = null;
1614
+ this.dispatcherRunning = false;
1245
1615
  }
1246
1616
  }
1247
1617
  }