@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/dist/client.d.ts +213 -9
- package/dist/client.js +424 -25
- package/dist/client.test.js +246 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/websocket.test.d.ts +6 -0
- package/dist/websocket.test.js +407 -0
- package/package.json +2 -1
- package/src/client.test.ts +341 -1
- package/src/client.ts +612 -30
- package/src/index.ts +10 -0
- package/src/websocket.test.ts +575 -0
- package/tsconfig.json +3 -1
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,
|
|
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
|
-
|
|
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
|
|
2247
|
+
* Connect and start the dispatcher.
|
|
1934
2248
|
*/
|
|
1935
|
-
private async
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
}
|