@ekodb/ekodb-client 0.12.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/src/client.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { encode, decode } from "@msgpack/msgpack";
6
6
  import { QueryBuilder, Query as QueryBuilderQuery } from "./query-builder";
7
- import { SearchQuery, SearchQueryBuilder, SearchResponse } from "./search";
7
+ import { SearchQuery, SearchResponse } from "./search";
8
8
  import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
9
9
  import { Script, FunctionResult } from "./functions";
10
10
 
@@ -46,8 +46,6 @@ export interface ClientConfig {
46
46
  shouldRetry?: boolean;
47
47
  /** Maximum number of retry attempts (default: 3) */
48
48
  maxRetries?: number;
49
- /** Request timeout in milliseconds (default: 30000) */
50
- timeout?: number;
51
49
  /** Serialization format (default: MessagePack for best performance, use Json for debugging) */
52
50
  format?: SerializationFormat;
53
51
  }
@@ -134,6 +132,26 @@ export interface BatchDeleteOptions {
134
132
  transactionId?: string;
135
133
  }
136
134
 
135
+ export interface DistinctValuesOptions {
136
+ /** Optional filter expression (same format as find() filter). */
137
+ filter?: any;
138
+ /** Bypass ripple propagation for this query. */
139
+ bypassRipple?: boolean;
140
+ /** Bypass cache for this query. */
141
+ bypassCache?: boolean;
142
+ }
143
+
144
+ export interface DistinctValuesResponse {
145
+ /** Collection that was queried. */
146
+ collection: string;
147
+ /** Field whose distinct values were returned. */
148
+ field: string;
149
+ /** Unique values, sorted alphabetically. */
150
+ values: any[];
151
+ /** Number of distinct values. */
152
+ count: number;
153
+ }
154
+
137
155
  // ========== Chat Interfaces ==========
138
156
 
139
157
  export interface CollectionConfig {
@@ -298,6 +316,24 @@ export interface EmbedResponse {
298
316
  dimensions: number;
299
317
  }
300
318
 
319
+ /**
320
+ * Request for stateless raw LLM completion — no session, no history, no RAG.
321
+ */
322
+ export interface RawCompletionRequest {
323
+ system_prompt: string;
324
+ message: string;
325
+ provider?: string;
326
+ model?: string;
327
+ max_tokens?: number;
328
+ }
329
+
330
+ /**
331
+ * Response from a raw LLM completion request.
332
+ */
333
+ export interface RawCompletionResponse {
334
+ content: string;
335
+ }
336
+
301
337
  /**
302
338
  * User function definition - reusable sequence of Functions that can be called by Scripts
303
339
  */
@@ -337,7 +373,6 @@ export class EkoDBClient {
337
373
  private token: string | null = null;
338
374
  private shouldRetry: boolean;
339
375
  private maxRetries: number;
340
- private timeout: number;
341
376
  private format: SerializationFormat;
342
377
  private rateLimitInfo: RateLimitInfo | null = null;
343
378
 
@@ -348,14 +383,12 @@ export class EkoDBClient {
348
383
  this.apiKey = apiKey!;
349
384
  this.shouldRetry = true;
350
385
  this.maxRetries = 3;
351
- this.timeout = 30000;
352
386
  this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
353
387
  } else {
354
388
  this.baseURL = config.baseURL;
355
389
  this.apiKey = config.apiKey;
356
390
  this.shouldRetry = config.shouldRetry ?? true;
357
391
  this.maxRetries = config.maxRetries ?? 3;
358
- this.timeout = config.timeout ?? 30000;
359
392
  this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
360
393
  }
361
394
  }
@@ -386,7 +419,7 @@ export class EkoDBClient {
386
419
  /**
387
420
  * Refresh the authentication token
388
421
  */
389
- private async refreshToken(): Promise<void> {
422
+ async refreshToken(): Promise<void> {
390
423
  const response = await fetch(`${this.baseURL}/api/auth/token`, {
391
424
  method: "POST",
392
425
  headers: { "Content-Type": "application/json" },
@@ -410,6 +443,22 @@ export class EkoDBClient {
410
443
  this.token = result.token;
411
444
  }
412
445
 
446
+ /**
447
+ * Get the current authentication token.
448
+ * Returns null if not yet authenticated. Call refreshToken() first.
449
+ */
450
+ getToken(): string | null {
451
+ return this.token;
452
+ }
453
+
454
+ /**
455
+ * Clear the cached authentication token.
456
+ * The next request will trigger a fresh token exchange.
457
+ */
458
+ clearTokenCache(): void {
459
+ this.token = null;
460
+ }
461
+
413
462
  /**
414
463
  * Extract rate limit information from response headers
415
464
  */
@@ -660,6 +709,32 @@ export class EkoDBClient {
660
709
  return this.makeRequest<Record>("GET", `/api/find/${collection}/${id}`);
661
710
  }
662
711
 
712
+ /**
713
+ * Find a document by ID with field projection
714
+ * @param collection - Collection name
715
+ * @param id - Document ID
716
+ * @param selectFields - Fields to include in the result
717
+ * @param excludeFields - Fields to exclude from the result
718
+ */
719
+ async findByIdWithProjection(
720
+ collection: string,
721
+ id: string,
722
+ selectFields?: string[],
723
+ excludeFields?: string[],
724
+ ): Promise<Record> {
725
+ const params = new URLSearchParams();
726
+ if (selectFields?.length) {
727
+ params.append("select_fields", selectFields.join(","));
728
+ }
729
+ if (excludeFields?.length) {
730
+ params.append("exclude_fields", excludeFields.join(","));
731
+ }
732
+ const url = params.toString()
733
+ ? `/api/find/${collection}/${id}?${params.toString()}`
734
+ : `/api/find/${collection}/${id}`;
735
+ return this.makeRequest<Record>("GET", url);
736
+ }
737
+
663
738
  /**
664
739
  * Update a document
665
740
  * @param collection - Collection name
@@ -688,6 +763,52 @@ export class EkoDBClient {
688
763
  return this.makeRequest<Record>("PUT", url, record);
689
764
  }
690
765
 
766
+ /**
767
+ * Apply an atomic field action to a single field of a record.
768
+ *
769
+ * Use this instead of `update()` for safe concurrent modifications like
770
+ * incrementing counters, pushing to arrays, or arithmetic operations.
771
+ *
772
+ * @param collection - Collection name
773
+ * @param id - Record ID
774
+ * @param action - The atomic action: increment, decrement, multiply, divide, modulo,
775
+ * push, pop, shift, unshift, remove, append, clear
776
+ * @param field - The field name to apply the action to
777
+ * @param value - The value for the action (omit for pop/shift/clear)
778
+ */
779
+ async updateWithAction(
780
+ collection: string,
781
+ id: string,
782
+ action: string,
783
+ field: string,
784
+ value?: any,
785
+ ): Promise<Record> {
786
+ const url = `/api/update/${collection}/${id}/action/${action}`;
787
+ return this.makeRequest<Record>("PUT", url, {
788
+ field,
789
+ value: value ?? null,
790
+ });
791
+ }
792
+
793
+ /**
794
+ * Apply a sequence of atomic field actions to a record in a single request.
795
+ *
796
+ * All actions are applied atomically — the record is fetched once, all actions
797
+ * run in order, and the result is persisted in a single update.
798
+ *
799
+ * @param collection - Collection name
800
+ * @param id - Record ID
801
+ * @param actions - Array of [action, field, value] tuples
802
+ */
803
+ async updateWithActionSequence(
804
+ collection: string,
805
+ id: string,
806
+ actions: [string, string, any][],
807
+ ): Promise<Record> {
808
+ const url = `/api/update/sequence/${collection}/${id}`;
809
+ return this.makeRequest<Record>("PUT", url, actions);
810
+ }
811
+
691
812
  /**
692
813
  * Delete a document
693
814
  * @param collection - Collection name
@@ -1313,6 +1434,51 @@ export class EkoDBClient {
1313
1434
  );
1314
1435
  }
1315
1436
 
1437
+ /**
1438
+ * Get distinct (unique) values for a field across all records in a collection.
1439
+ *
1440
+ * Results are deduplicated and sorted alphabetically. Supports an optional filter
1441
+ * to restrict which records are examined.
1442
+ *
1443
+ * @param collection - Collection name
1444
+ * @param field - Field to get distinct values for
1445
+ * @param options - Optional filter and bypass flags
1446
+ *
1447
+ * @example
1448
+ * // All distinct statuses
1449
+ * const resp = await client.distinctValues("orders", "status");
1450
+ * console.log(resp.values); // ["active", "cancelled", "shipped"]
1451
+ *
1452
+ * // Only statuses for US orders
1453
+ * const resp = await client.distinctValues("orders", "status", {
1454
+ * filter: { type: "Condition", content: { field: "region", operator: "Eq", value: "us" } }
1455
+ * });
1456
+ */
1457
+ async distinctValues(
1458
+ collection: string,
1459
+ field: string,
1460
+ options: DistinctValuesOptions = {},
1461
+ ): Promise<DistinctValuesResponse> {
1462
+ const body: {
1463
+ filter?: any;
1464
+ bypass_ripple?: boolean;
1465
+ bypass_cache?: boolean;
1466
+ } = {};
1467
+ if (options.filter !== undefined) body.filter = options.filter;
1468
+ if (options.bypassRipple !== undefined)
1469
+ body.bypass_ripple = options.bypassRipple;
1470
+ if (options.bypassCache !== undefined)
1471
+ body.bypass_cache = options.bypassCache;
1472
+
1473
+ return this.makeRequest<DistinctValuesResponse>(
1474
+ "POST",
1475
+ `/api/distinct/${collection}/${field}`,
1476
+ body,
1477
+ 0,
1478
+ true, // Force JSON
1479
+ );
1480
+ }
1481
+
1316
1482
  /**
1317
1483
  * Health check - verify the ekoDB server is responding
1318
1484
  */
@@ -1348,6 +1514,34 @@ export class EkoDBClient {
1348
1514
  );
1349
1515
  }
1350
1516
 
1517
+ /**
1518
+ * Stateless raw LLM completion — no session, no history, no RAG.
1519
+ *
1520
+ * Sends a system prompt and user message directly to the LLM via ekoDB
1521
+ * and returns the raw text response without any context injection or
1522
+ * conversation management. Use this for structured-output tasks such as
1523
+ * planning where the response must be parsed programmatically.
1524
+ *
1525
+ * @example
1526
+ * const resp = await client.rawCompletion({
1527
+ * system_prompt: "You are a helpful assistant.",
1528
+ * message: "Summarize this in JSON.",
1529
+ * max_tokens: 2048,
1530
+ * });
1531
+ * console.log(resp.content);
1532
+ */
1533
+ async rawCompletion(
1534
+ request: RawCompletionRequest,
1535
+ ): Promise<RawCompletionResponse> {
1536
+ return this.makeRequest<RawCompletionResponse>(
1537
+ "POST",
1538
+ "/api/chat/complete",
1539
+ request,
1540
+ 0,
1541
+ true, // Force JSON
1542
+ );
1543
+ }
1544
+
1351
1545
  /**
1352
1546
  * Send a message in an existing chat session
1353
1547
  */
@@ -1560,6 +1754,21 @@ export class EkoDBClient {
1560
1754
  );
1561
1755
  }
1562
1756
 
1757
+ /**
1758
+ * Get all built-in server-side chat tool definitions.
1759
+ * Returns a list of tool objects with name, description, and parameters fields.
1760
+ * Used by planning agents to discover available tools dynamically.
1761
+ */
1762
+ async getChatTools(): Promise<object[]> {
1763
+ return this.makeRequest<object[]>(
1764
+ "GET",
1765
+ "/api/chat/tools",
1766
+ undefined,
1767
+ 0,
1768
+ true, // Force JSON
1769
+ );
1770
+ }
1771
+
1563
1772
  /**
1564
1773
  * Get available models for a specific provider
1565
1774
  * @param provider - Provider name (e.g., "openai", "anthropic", "perplexity")
@@ -1916,26 +2125,130 @@ export class EkoDBClient {
1916
2125
  }
1917
2126
  }
1918
2127
 
2128
+ /** Mutation notification from a subscription. */
2129
+ export interface MutationNotification {
2130
+ collection: string;
2131
+ event: string;
2132
+ recordIds: string[];
2133
+ records?: any;
2134
+ timestamp: string;
2135
+ }
2136
+
2137
+ /** A chunk/event from a streaming chat response. */
2138
+ export type ChatStreamEvent =
2139
+ | { type: "chunk"; content: string }
2140
+ | {
2141
+ type: "end";
2142
+ messageId: string;
2143
+ tokenUsage?: any;
2144
+ toolCallHistory?: any;
2145
+ executionTimeMs: number;
2146
+ }
2147
+ | {
2148
+ type: "toolCall";
2149
+ chatId: string;
2150
+ callId: string;
2151
+ toolName: string;
2152
+ arguments: any;
2153
+ }
2154
+ | { type: "error"; error: string };
2155
+
2156
+ /** Definition for a client-side tool the LLM can call. */
2157
+ export interface ClientToolDefinition {
2158
+ name: string;
2159
+ description: string;
2160
+ parameters: any;
2161
+ }
2162
+
2163
+ /** Options for chatSend. */
2164
+ export interface ChatSendOptions {
2165
+ bypassRipple?: boolean;
2166
+ clientTools?: ClientToolDefinition[];
2167
+ maxIterations?: number;
2168
+ confirmTools?: string[];
2169
+ excludeTools?: string[];
2170
+ }
2171
+
2172
+ /** Options for subscribe. */
2173
+ export interface SubscribeOptions {
2174
+ filterField?: string;
2175
+ filterValue?: string;
2176
+ }
2177
+
2178
+ /** EventEmitter-like interface for subscriptions and chat streams. */
2179
+ export class EventStream<_T = unknown> {
2180
+ private listeners: Map<string, Array<(data: any) => void>> = new Map();
2181
+ private _closed = false;
2182
+
2183
+ on(event: string, listener: (data: any) => void): this {
2184
+ if (!this.listeners.has(event)) {
2185
+ this.listeners.set(event, []);
2186
+ }
2187
+ this.listeners.get(event)!.push(listener);
2188
+ return this;
2189
+ }
2190
+
2191
+ /** @internal */
2192
+ emit(event: string, data?: any): void {
2193
+ const handlers = this.listeners.get(event);
2194
+ if (handlers) {
2195
+ for (const handler of handlers) {
2196
+ handler(data);
2197
+ }
2198
+ }
2199
+ }
2200
+
2201
+ get closed(): boolean {
2202
+ return this._closed;
2203
+ }
2204
+
2205
+ /** @internal */
2206
+ close(): void {
2207
+ this._closed = true;
2208
+ this.emit("close");
2209
+ }
2210
+ }
2211
+
1919
2212
  /**
1920
- * WebSocket client for real-time queries
2213
+ * WebSocket client for real-time queries, subscriptions, and chat streaming.
1921
2214
  */
1922
2215
  export class WebSocketClient {
1923
2216
  private wsURL: string;
1924
2217
  private token: string;
1925
2218
  private ws: any = null;
2219
+ private dispatcherRunning = false;
2220
+
2221
+ // Dispatcher state
2222
+ private pendingRequests: Map<
2223
+ string,
2224
+ { resolve: (value: any) => void; reject: (reason: any) => void }
2225
+ > = new Map();
2226
+ private subscriptions: Map<string, EventStream<MutationNotification>> =
2227
+ new Map();
2228
+ private chatStreams: Map<string, EventStream<ChatStreamEvent>> = new Map();
2229
+ private registerToolsAck: {
2230
+ resolve: (value: any) => void;
2231
+ reject: (reason: any) => void;
2232
+ } | null = null;
1926
2233
 
1927
2234
  constructor(wsURL: string, token: string) {
1928
2235
  this.wsURL = wsURL;
1929
2236
  this.token = token;
1930
2237
  }
1931
2238
 
2239
+ private messageCounter = 0;
2240
+
2241
+ private genMessageId(): string {
2242
+ const counter = this.messageCounter++;
2243
+ return `${Date.now()}-${counter}-${Math.random().toString(36).slice(2, 8)}`;
2244
+ }
2245
+
1932
2246
  /**
1933
- * Connect to WebSocket
2247
+ * Connect and start the dispatcher.
1934
2248
  */
1935
- private async connect(): Promise<void> {
1936
- if (this.ws) return;
2249
+ private async ensureConnected(): Promise<void> {
2250
+ if (this.ws && this.dispatcherRunning) return;
1937
2251
 
1938
- // Dynamic import for Node.js WebSocket
1939
2252
  const WebSocket = (await import("ws")).default;
1940
2253
 
1941
2254
  let url = this.wsURL;
@@ -1949,49 +2262,318 @@ export class WebSocketClient {
1949
2262
  },
1950
2263
  });
1951
2264
 
1952
- return new Promise((resolve, reject) => {
2265
+ await new Promise<void>((resolve, reject) => {
1953
2266
  this.ws.on("open", () => resolve());
1954
2267
  this.ws.on("error", (err: Error) => reject(err));
1955
2268
  });
2269
+
2270
+ this.spawnDispatcher();
2271
+ }
2272
+
2273
+ private spawnDispatcher(): void {
2274
+ if (this.dispatcherRunning) return;
2275
+ this.dispatcherRunning = true;
2276
+
2277
+ this.ws.on("message", (data: Buffer) => {
2278
+ try {
2279
+ const msg = JSON.parse(data.toString());
2280
+ this.routeMessage(msg);
2281
+ } catch {
2282
+ // Ignore malformed messages
2283
+ }
2284
+ });
2285
+
2286
+ this.ws.on("close", () => {
2287
+ this.dispatcherRunning = false;
2288
+ // Notify all pending requests
2289
+ for (const [, pending] of this.pendingRequests) {
2290
+ pending.reject(new Error("WebSocket connection closed"));
2291
+ }
2292
+ this.pendingRequests.clear();
2293
+ // Close all chat streams
2294
+ for (const [, stream] of this.chatStreams) {
2295
+ stream.emit("event", { type: "error", error: "Connection closed" });
2296
+ stream.close();
2297
+ }
2298
+ this.chatStreams.clear();
2299
+ // Close all subscriptions
2300
+ for (const [, stream] of this.subscriptions) {
2301
+ stream.close();
2302
+ }
2303
+ this.subscriptions.clear();
2304
+ this.ws = null;
2305
+ });
2306
+ }
2307
+
2308
+ private routeMessage(msg: any): void {
2309
+ switch (msg.type) {
2310
+ case "Success":
2311
+ case "Error": {
2312
+ const messageId = msg.payload?.message_id || msg.payload?.messageId;
2313
+ if (messageId && this.pendingRequests.has(messageId)) {
2314
+ const pending = this.pendingRequests.get(messageId)!;
2315
+ this.pendingRequests.delete(messageId);
2316
+ if (msg.type === "Error") {
2317
+ pending.reject(new Error(msg.message || "Unknown error"));
2318
+ } else {
2319
+ pending.resolve(msg.payload);
2320
+ }
2321
+ } else if (this.registerToolsAck) {
2322
+ const ack = this.registerToolsAck;
2323
+ this.registerToolsAck = null;
2324
+ if (msg.type === "Error") {
2325
+ ack.reject(new Error(msg.message || "Tool registration failed"));
2326
+ } else {
2327
+ ack.resolve(msg.payload);
2328
+ }
2329
+ }
2330
+ break;
2331
+ }
2332
+
2333
+ case "MutationNotification": {
2334
+ const payload = msg.payload;
2335
+ const notification: MutationNotification = {
2336
+ collection: payload.collection,
2337
+ event: payload.event,
2338
+ recordIds: payload.record_ids || payload.recordIds || [],
2339
+ records: payload.records,
2340
+ timestamp: payload.timestamp,
2341
+ };
2342
+ for (const [collection, stream] of this.subscriptions) {
2343
+ if (collection === notification.collection) {
2344
+ stream.emit("mutation", notification);
2345
+ }
2346
+ }
2347
+ break;
2348
+ }
2349
+
2350
+ case "ChatStreamChunk": {
2351
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
2352
+ const stream = this.chatStreams.get(chatId);
2353
+ if (stream) {
2354
+ stream.emit("event", {
2355
+ type: "chunk",
2356
+ content: msg.payload.content,
2357
+ } as ChatStreamEvent);
2358
+ }
2359
+ break;
2360
+ }
2361
+
2362
+ case "ChatStreamEnd": {
2363
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
2364
+ const stream = this.chatStreams.get(chatId);
2365
+ if (stream) {
2366
+ stream.emit("event", {
2367
+ type: "end",
2368
+ messageId: msg.payload.message_id || msg.payload.messageId || "",
2369
+ tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
2370
+ toolCallHistory:
2371
+ msg.payload.tool_call_history || msg.payload.toolCallHistory,
2372
+ executionTimeMs:
2373
+ msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
2374
+ } as ChatStreamEvent);
2375
+ this.chatStreams.delete(chatId);
2376
+ stream.close();
2377
+ }
2378
+ break;
2379
+ }
2380
+
2381
+ case "ChatStreamError": {
2382
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
2383
+ const stream = this.chatStreams.get(chatId);
2384
+ if (stream) {
2385
+ stream.emit("event", {
2386
+ type: "error",
2387
+ error: msg.payload.error || msg.payload.message || "Unknown error",
2388
+ } as ChatStreamEvent);
2389
+ this.chatStreams.delete(chatId);
2390
+ stream.close();
2391
+ }
2392
+ break;
2393
+ }
2394
+
2395
+ case "ClientToolCall": {
2396
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
2397
+ const stream = this.chatStreams.get(chatId);
2398
+ if (stream) {
2399
+ stream.emit("event", {
2400
+ type: "toolCall",
2401
+ chatId,
2402
+ callId: msg.payload.call_id || msg.payload.callId,
2403
+ toolName: msg.payload.tool_name || msg.payload.toolName,
2404
+ arguments: msg.payload.arguments,
2405
+ } as ChatStreamEvent);
2406
+ }
2407
+ break;
2408
+ }
2409
+ }
2410
+ }
2411
+
2412
+ private async sendRequest(request: any): Promise<any> {
2413
+ await this.ensureConnected();
2414
+ const messageId = request.messageId || request.message_id;
2415
+
2416
+ return new Promise((resolve, reject) => {
2417
+ this.pendingRequests.set(messageId, { resolve, reject });
2418
+ try {
2419
+ this.ws.send(JSON.stringify(request));
2420
+ } catch (err) {
2421
+ this.pendingRequests.delete(messageId);
2422
+ reject(err);
2423
+ }
2424
+ });
1956
2425
  }
1957
2426
 
1958
2427
  /**
1959
- * Find all records in a collection via WebSocket
2428
+ * Find all records in a collection via WebSocket.
1960
2429
  */
1961
2430
  async findAll(collection: string): Promise<Record[]> {
1962
- await this.connect();
1963
-
1964
- const messageId = Date.now().toString();
1965
- const request = {
2431
+ const messageId = this.genMessageId();
2432
+ const payload = await this.sendRequest({
1966
2433
  type: "FindAll",
1967
2434
  messageId,
1968
2435
  payload: { collection },
2436
+ });
2437
+ return payload?.data || [];
2438
+ }
2439
+
2440
+ /**
2441
+ * Subscribe to mutation notifications on a collection.
2442
+ * Returns an EventStream that emits "mutation" events.
2443
+ */
2444
+ async subscribe(
2445
+ collection: string,
2446
+ options?: SubscribeOptions,
2447
+ ): Promise<EventStream<MutationNotification>> {
2448
+ await this.ensureConnected();
2449
+
2450
+ if (this.subscriptions.has(collection)) {
2451
+ throw new Error(`Already subscribed to collection "${collection}"`);
2452
+ }
2453
+
2454
+ const messageId = this.genMessageId();
2455
+ const stream = new EventStream<MutationNotification>();
2456
+ this.subscriptions.set(collection, stream);
2457
+
2458
+ const request: any = {
2459
+ type: "Subscribe",
2460
+ messageId,
2461
+ payload: {
2462
+ collection,
2463
+ ...(options?.filterField && { filter_field: options.filterField }),
2464
+ ...(options?.filterValue && { filter_value: options.filterValue }),
2465
+ },
1969
2466
  };
1970
2467
 
1971
- return new Promise((resolve, reject) => {
1972
- this.ws.send(JSON.stringify(request));
2468
+ // Send subscribe request and wait for ack
2469
+ try {
2470
+ await this.sendRequest(request);
2471
+ } catch (err) {
2472
+ this.subscriptions.delete(collection);
2473
+ throw err;
2474
+ }
2475
+ return stream;
2476
+ }
1973
2477
 
1974
- this.ws.once("message", (data: Buffer) => {
1975
- const response = JSON.parse(data.toString());
2478
+ /**
2479
+ * Send a chat message and receive a streaming response.
2480
+ * Returns an EventStream that emits "event" with ChatStreamEvent objects.
2481
+ */
2482
+ async chatSend(
2483
+ chatId: string,
2484
+ message: string,
2485
+ options?: ChatSendOptions,
2486
+ ): Promise<EventStream<ChatStreamEvent>> {
2487
+ await this.ensureConnected();
1976
2488
 
1977
- if (response.type === "Error") {
1978
- reject(new Error(response.message));
1979
- } else {
1980
- resolve(response.payload?.data || []);
1981
- }
1982
- });
2489
+ if (this.chatStreams.has(chatId)) {
2490
+ throw new Error(`Chat stream already active for chatId "${chatId}"`);
2491
+ }
2492
+
2493
+ const stream = new EventStream<ChatStreamEvent>();
2494
+ this.chatStreams.set(chatId, stream);
2495
+
2496
+ const request: any = {
2497
+ type: "ChatSend",
2498
+ payload: {
2499
+ chat_id: chatId,
2500
+ message,
2501
+ ...(options?.bypassRipple != null && {
2502
+ bypass_ripple: options.bypassRipple,
2503
+ }),
2504
+ ...(options?.clientTools && { client_tools: options.clientTools }),
2505
+ ...(options?.maxIterations != null && {
2506
+ max_iterations: options.maxIterations,
2507
+ }),
2508
+ ...(options?.confirmTools && { confirm_tools: options.confirmTools }),
2509
+ ...(options?.excludeTools && { exclude_tools: options.excludeTools }),
2510
+ },
2511
+ };
2512
+
2513
+ this.ws.send(JSON.stringify(request));
2514
+ return stream;
2515
+ }
1983
2516
 
1984
- this.ws.once("error", reject);
2517
+ /**
2518
+ * Register client-side tools for a chat session.
2519
+ */
2520
+ async registerClientTools(
2521
+ chatId: string,
2522
+ tools: ClientToolDefinition[],
2523
+ ): Promise<void> {
2524
+ await this.ensureConnected();
2525
+
2526
+ const request = {
2527
+ type: "RegisterClientTools",
2528
+ payload: {
2529
+ chat_id: chatId,
2530
+ tools,
2531
+ },
2532
+ };
2533
+
2534
+ await new Promise<void>((resolve, reject) => {
2535
+ this.registerToolsAck = {
2536
+ resolve: () => resolve(),
2537
+ reject: (err) => reject(err),
2538
+ };
2539
+ this.ws.send(JSON.stringify(request));
1985
2540
  });
1986
2541
  }
1987
2542
 
1988
2543
  /**
1989
- * Close the WebSocket connection
2544
+ * Send a tool result back to the server during a chat stream.
2545
+ */
2546
+ async sendToolResult(
2547
+ chatId: string,
2548
+ callId: string,
2549
+ success: boolean,
2550
+ result?: any,
2551
+ error?: string,
2552
+ ): Promise<void> {
2553
+ await this.ensureConnected();
2554
+
2555
+ const request = {
2556
+ type: "ClientToolResult",
2557
+ payload: {
2558
+ chat_id: chatId,
2559
+ call_id: callId,
2560
+ success,
2561
+ ...(result !== undefined && { result }),
2562
+ ...(error !== undefined && { error }),
2563
+ },
2564
+ };
2565
+
2566
+ this.ws.send(JSON.stringify(request));
2567
+ }
2568
+
2569
+ /**
2570
+ * Close the WebSocket connection.
1990
2571
  */
1991
2572
  close(): void {
1992
2573
  if (this.ws) {
1993
2574
  this.ws.close();
1994
2575
  this.ws = null;
2576
+ this.dispatcherRunning = false;
1995
2577
  }
1996
2578
  }
1997
2579
  }