@ekodb/ekodb-client 0.12.0 → 0.14.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/README.md +70 -0
- package/dist/client.d.ts +351 -9
- package/dist/client.js +903 -25
- package/dist/client.test.js +1056 -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 +496 -0
- package/package.json +2 -1
- package/src/client.test.ts +1357 -1
- package/src/client.ts +1484 -86
- package/src/index.ts +10 -0
- package/src/websocket.test.ts +712 -0
- package/tsconfig.json +3 -1
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");
|
|
@@ -71,6 +71,7 @@ var MergeStrategy;
|
|
|
71
71
|
class EkoDBClient {
|
|
72
72
|
constructor(config, apiKey) {
|
|
73
73
|
this.token = null;
|
|
74
|
+
this.tokenExpiry = 0;
|
|
74
75
|
this.rateLimitInfo = null;
|
|
75
76
|
// Support both old (baseURL, apiKey) and new (config object) signatures
|
|
76
77
|
if (typeof config === "string") {
|
|
@@ -78,7 +79,6 @@ class EkoDBClient {
|
|
|
78
79
|
this.apiKey = apiKey;
|
|
79
80
|
this.shouldRetry = true;
|
|
80
81
|
this.maxRetries = 3;
|
|
81
|
-
this.timeout = 30000;
|
|
82
82
|
this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
@@ -86,7 +86,6 @@ class EkoDBClient {
|
|
|
86
86
|
this.apiKey = config.apiKey;
|
|
87
87
|
this.shouldRetry = config.shouldRetry ?? true;
|
|
88
88
|
this.maxRetries = config.maxRetries ?? 3;
|
|
89
|
-
this.timeout = config.timeout ?? 30000;
|
|
90
89
|
this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -135,6 +134,68 @@ class EkoDBClient {
|
|
|
135
134
|
}
|
|
136
135
|
const result = (await response.json());
|
|
137
136
|
this.token = result.token;
|
|
137
|
+
// Extract and cache JWT expiry for proactive refresh
|
|
138
|
+
const expiry = this.extractJWTExpiry(result.token);
|
|
139
|
+
this.tokenExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600; // fallback: 1 hour
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get a valid authentication token.
|
|
143
|
+
*
|
|
144
|
+
* Returns a cached token if it has more than 60s of validity remaining.
|
|
145
|
+
* Otherwise fetches a new one via refreshToken(). This means callers
|
|
146
|
+
* never need to handle token refresh themselves — every getToken() call
|
|
147
|
+
* returns a token that's valid for at least 60 more seconds.
|
|
148
|
+
*/
|
|
149
|
+
async getToken() {
|
|
150
|
+
if (this.token) {
|
|
151
|
+
const now = Math.floor(Date.now() / 1000);
|
|
152
|
+
if (now + 60 >= this.tokenExpiry) {
|
|
153
|
+
// Token is about to expire or already expired — refresh proactively
|
|
154
|
+
await this.refreshToken();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// No token yet — fetch one
|
|
159
|
+
await this.refreshToken();
|
|
160
|
+
}
|
|
161
|
+
return this.token;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Clear the cached authentication token and expiry.
|
|
165
|
+
* The next request will trigger a fresh token exchange.
|
|
166
|
+
*/
|
|
167
|
+
clearTokenCache() {
|
|
168
|
+
this.token = null;
|
|
169
|
+
this.tokenExpiry = 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extract the `exp` claim from a JWT without verifying the signature.
|
|
173
|
+
* Returns the Unix timestamp (seconds) of expiry, or null if parsing fails.
|
|
174
|
+
*/
|
|
175
|
+
extractJWTExpiry(token) {
|
|
176
|
+
try {
|
|
177
|
+
const parts = token.split(".");
|
|
178
|
+
if (parts.length !== 3) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
// Convert base64url to standard base64
|
|
182
|
+
let payload = parts[1];
|
|
183
|
+
payload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
184
|
+
// Pad to multiple of 4
|
|
185
|
+
const pad = payload.length % 4;
|
|
186
|
+
if (pad) {
|
|
187
|
+
payload += "=".repeat(4 - pad);
|
|
188
|
+
}
|
|
189
|
+
const decoded = atob(payload);
|
|
190
|
+
const claims = JSON.parse(decoded);
|
|
191
|
+
if (typeof claims.exp === "number") {
|
|
192
|
+
return claims.exp;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
138
199
|
}
|
|
139
200
|
/**
|
|
140
201
|
* Extract rate limit information from response headers
|
|
@@ -326,6 +387,26 @@ class EkoDBClient {
|
|
|
326
387
|
async findById(collection, id) {
|
|
327
388
|
return this.makeRequest("GET", `/api/find/${collection}/${id}`);
|
|
328
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Find a document by ID with field projection
|
|
392
|
+
* @param collection - Collection name
|
|
393
|
+
* @param id - Document ID
|
|
394
|
+
* @param selectFields - Fields to include in the result
|
|
395
|
+
* @param excludeFields - Fields to exclude from the result
|
|
396
|
+
*/
|
|
397
|
+
async findByIdWithProjection(collection, id, selectFields, excludeFields) {
|
|
398
|
+
const params = new URLSearchParams();
|
|
399
|
+
if (selectFields?.length) {
|
|
400
|
+
params.append("select_fields", selectFields.join(","));
|
|
401
|
+
}
|
|
402
|
+
if (excludeFields?.length) {
|
|
403
|
+
params.append("exclude_fields", excludeFields.join(","));
|
|
404
|
+
}
|
|
405
|
+
const url = params.toString()
|
|
406
|
+
? `/api/find/${collection}/${id}?${params.toString()}`
|
|
407
|
+
: `/api/find/${collection}/${id}`;
|
|
408
|
+
return this.makeRequest("GET", url);
|
|
409
|
+
}
|
|
329
410
|
/**
|
|
330
411
|
* Update a document
|
|
331
412
|
* @param collection - Collection name
|
|
@@ -346,6 +427,40 @@ class EkoDBClient {
|
|
|
346
427
|
: `/api/update/${collection}/${id}`;
|
|
347
428
|
return this.makeRequest("PUT", url, record);
|
|
348
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Apply an atomic field action to a single field of a record.
|
|
432
|
+
*
|
|
433
|
+
* Use this instead of `update()` for safe concurrent modifications like
|
|
434
|
+
* incrementing counters, pushing to arrays, or arithmetic operations.
|
|
435
|
+
*
|
|
436
|
+
* @param collection - Collection name
|
|
437
|
+
* @param id - Record ID
|
|
438
|
+
* @param action - The atomic action: increment, decrement, multiply, divide, modulo,
|
|
439
|
+
* push, pop, shift, unshift, remove, append, clear
|
|
440
|
+
* @param field - The field name to apply the action to
|
|
441
|
+
* @param value - The value for the action (omit for pop/shift/clear)
|
|
442
|
+
*/
|
|
443
|
+
async updateWithAction(collection, id, action, field, value) {
|
|
444
|
+
const url = `/api/update/${collection}/${id}/action/${action}`;
|
|
445
|
+
return this.makeRequest("PUT", url, {
|
|
446
|
+
field,
|
|
447
|
+
value: value ?? null,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Apply a sequence of atomic field actions to a record in a single request.
|
|
452
|
+
*
|
|
453
|
+
* All actions are applied atomically — the record is fetched once, all actions
|
|
454
|
+
* run in order, and the result is persisted in a single update.
|
|
455
|
+
*
|
|
456
|
+
* @param collection - Collection name
|
|
457
|
+
* @param id - Record ID
|
|
458
|
+
* @param actions - Array of [action, field, value] tuples
|
|
459
|
+
*/
|
|
460
|
+
async updateWithActionSequence(collection, id, actions) {
|
|
461
|
+
const url = `/api/update/sequence/${collection}/${id}`;
|
|
462
|
+
return this.makeRequest("PUT", url, actions);
|
|
463
|
+
}
|
|
349
464
|
/**
|
|
350
465
|
* Delete a document
|
|
351
466
|
* @param collection - Collection name
|
|
@@ -772,6 +887,36 @@ class EkoDBClient {
|
|
|
772
887
|
// Ensure all parameters from SearchQuery are sent to server
|
|
773
888
|
return this.makeRequest("POST", `/api/search/${collection}`, query, 0, true);
|
|
774
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Get distinct (unique) values for a field across all records in a collection.
|
|
892
|
+
*
|
|
893
|
+
* Results are deduplicated and sorted alphabetically. Supports an optional filter
|
|
894
|
+
* to restrict which records are examined.
|
|
895
|
+
*
|
|
896
|
+
* @param collection - Collection name
|
|
897
|
+
* @param field - Field to get distinct values for
|
|
898
|
+
* @param options - Optional filter and bypass flags
|
|
899
|
+
*
|
|
900
|
+
* @example
|
|
901
|
+
* // All distinct statuses
|
|
902
|
+
* const resp = await client.distinctValues("orders", "status");
|
|
903
|
+
* console.log(resp.values); // ["active", "cancelled", "shipped"]
|
|
904
|
+
*
|
|
905
|
+
* // Only statuses for US orders
|
|
906
|
+
* const resp = await client.distinctValues("orders", "status", {
|
|
907
|
+
* filter: { type: "Condition", content: { field: "region", operator: "Eq", value: "us" } }
|
|
908
|
+
* });
|
|
909
|
+
*/
|
|
910
|
+
async distinctValues(collection, field, options = {}) {
|
|
911
|
+
const body = {};
|
|
912
|
+
if (options.filter !== undefined)
|
|
913
|
+
body.filter = options.filter;
|
|
914
|
+
if (options.bypassRipple !== undefined)
|
|
915
|
+
body.bypass_ripple = options.bypassRipple;
|
|
916
|
+
if (options.bypassCache !== undefined)
|
|
917
|
+
body.bypass_cache = options.bypassCache;
|
|
918
|
+
return this.makeRequest("POST", `/api/distinct/${collection}/${field}`, body, 0, true);
|
|
919
|
+
}
|
|
775
920
|
/**
|
|
776
921
|
* Health check - verify the ekoDB server is responding
|
|
777
922
|
*/
|
|
@@ -791,12 +936,217 @@ class EkoDBClient {
|
|
|
791
936
|
async createChatSession(request) {
|
|
792
937
|
return this.makeRequest("POST", "/api/chat", request, 0, true);
|
|
793
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* Stateless raw LLM completion — no session, no history, no RAG.
|
|
941
|
+
*
|
|
942
|
+
* Sends a system prompt and user message directly to the LLM via ekoDB
|
|
943
|
+
* and returns the raw text response without any context injection or
|
|
944
|
+
* conversation management. Use this for structured-output tasks such as
|
|
945
|
+
* planning where the response must be parsed programmatically.
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* const resp = await client.rawCompletion({
|
|
949
|
+
* system_prompt: "You are a helpful assistant.",
|
|
950
|
+
* message: "Summarize this in JSON.",
|
|
951
|
+
* max_tokens: 2048,
|
|
952
|
+
* });
|
|
953
|
+
* console.log(resp.content);
|
|
954
|
+
*/
|
|
955
|
+
async rawCompletion(request) {
|
|
956
|
+
return this.makeRequest("POST", "/api/chat/complete", request, 0, true);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Stateless raw LLM completion via SSE streaming.
|
|
960
|
+
*
|
|
961
|
+
* Same as rawCompletion() but uses Server-Sent Events to keep the
|
|
962
|
+
* connection alive. Preferred for deployed instances where reverse proxies
|
|
963
|
+
* may kill idle HTTP connections before the LLM responds.
|
|
964
|
+
*/
|
|
965
|
+
async rawCompletionStream(request) {
|
|
966
|
+
let token = await this.getToken();
|
|
967
|
+
const url = `${this.baseURL}/api/chat/complete/stream`;
|
|
968
|
+
const response = await fetch(url, {
|
|
969
|
+
method: "POST",
|
|
970
|
+
headers: {
|
|
971
|
+
"Content-Type": "application/json",
|
|
972
|
+
Accept: "text/event-stream",
|
|
973
|
+
Authorization: `Bearer ${token}`,
|
|
974
|
+
},
|
|
975
|
+
body: JSON.stringify(request),
|
|
976
|
+
});
|
|
977
|
+
if (!response.ok) {
|
|
978
|
+
const body = await response.text();
|
|
979
|
+
throw new Error(`SSE raw completion failed (${response.status}): ${body}`);
|
|
980
|
+
}
|
|
981
|
+
const body = await response.text();
|
|
982
|
+
let content = "";
|
|
983
|
+
let lastError = null;
|
|
984
|
+
for (const line of body.split("\n")) {
|
|
985
|
+
if (line.startsWith("data:")) {
|
|
986
|
+
const dataStr = line.slice(5).trim();
|
|
987
|
+
if (!dataStr)
|
|
988
|
+
continue;
|
|
989
|
+
try {
|
|
990
|
+
const eventData = JSON.parse(dataStr);
|
|
991
|
+
if (eventData.token)
|
|
992
|
+
content += eventData.token;
|
|
993
|
+
if (eventData.content)
|
|
994
|
+
content = eventData.content;
|
|
995
|
+
if (eventData.error)
|
|
996
|
+
lastError = eventData.error;
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// skip malformed SSE data
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (lastError)
|
|
1004
|
+
throw new Error(lastError);
|
|
1005
|
+
return { content };
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Stateless raw LLM completion via SSE streaming with token-level progress.
|
|
1009
|
+
*
|
|
1010
|
+
* Same as rawCompletionStream() but invokes `onToken` with each token as it
|
|
1011
|
+
* arrives, allowing callers to show real-time progress.
|
|
1012
|
+
*/
|
|
1013
|
+
async rawCompletionStreamWithProgress(request, onToken) {
|
|
1014
|
+
let token = await this.getToken();
|
|
1015
|
+
const url = `${this.baseURL}/api/chat/complete/stream`;
|
|
1016
|
+
const response = await fetch(url, {
|
|
1017
|
+
method: "POST",
|
|
1018
|
+
headers: {
|
|
1019
|
+
"Content-Type": "application/json",
|
|
1020
|
+
Accept: "text/event-stream",
|
|
1021
|
+
Authorization: `Bearer ${token}`,
|
|
1022
|
+
},
|
|
1023
|
+
body: JSON.stringify(request),
|
|
1024
|
+
});
|
|
1025
|
+
if (!response.ok) {
|
|
1026
|
+
const body = await response.text();
|
|
1027
|
+
throw new Error(`SSE raw completion failed (${response.status}): ${body}`);
|
|
1028
|
+
}
|
|
1029
|
+
const body = await response.text();
|
|
1030
|
+
let content = "";
|
|
1031
|
+
let lastError = null;
|
|
1032
|
+
for (const line of body.split("\n")) {
|
|
1033
|
+
if (line.startsWith("data:")) {
|
|
1034
|
+
const dataStr = line.slice(5).trim();
|
|
1035
|
+
if (!dataStr)
|
|
1036
|
+
continue;
|
|
1037
|
+
try {
|
|
1038
|
+
const eventData = JSON.parse(dataStr);
|
|
1039
|
+
if (eventData.token) {
|
|
1040
|
+
content += eventData.token;
|
|
1041
|
+
onToken(eventData.token);
|
|
1042
|
+
}
|
|
1043
|
+
if (eventData.content)
|
|
1044
|
+
content = eventData.content;
|
|
1045
|
+
if (eventData.error)
|
|
1046
|
+
lastError = eventData.error;
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
// skip malformed SSE data
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (lastError)
|
|
1054
|
+
throw new Error(lastError);
|
|
1055
|
+
return { content };
|
|
1056
|
+
}
|
|
794
1057
|
/**
|
|
795
1058
|
* Send a message in an existing chat session
|
|
796
1059
|
*/
|
|
797
1060
|
async chatMessage(sessionId, request) {
|
|
798
1061
|
return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
|
|
799
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Send a message in an existing chat session via SSE streaming.
|
|
1065
|
+
*
|
|
1066
|
+
* Returns an EventStream that emits ChatStreamEvent objects as they arrive:
|
|
1067
|
+
* - `{ type: "chunk", content: "..." }` for each token
|
|
1068
|
+
* - `{ type: "end", messageId, executionTimeMs, tokenUsage?, contextWindow? }` when complete
|
|
1069
|
+
* - `{ type: "error", error: "..." }` on failure
|
|
1070
|
+
*
|
|
1071
|
+
* Preferred over chatMessage() for long-running responses where reverse
|
|
1072
|
+
* proxies may kill idle HTTP connections before the LLM responds.
|
|
1073
|
+
*/
|
|
1074
|
+
chatMessageStream(chatId, request) {
|
|
1075
|
+
const stream = new EventStream();
|
|
1076
|
+
(async () => {
|
|
1077
|
+
try {
|
|
1078
|
+
let token = this.getToken();
|
|
1079
|
+
if (!token) {
|
|
1080
|
+
await this.refreshToken();
|
|
1081
|
+
token = this.getToken();
|
|
1082
|
+
}
|
|
1083
|
+
const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
|
|
1084
|
+
const response = await fetch(url, {
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
headers: {
|
|
1087
|
+
"Content-Type": "application/json",
|
|
1088
|
+
Accept: "text/event-stream",
|
|
1089
|
+
Authorization: `Bearer ${token}`,
|
|
1090
|
+
},
|
|
1091
|
+
body: JSON.stringify(request),
|
|
1092
|
+
});
|
|
1093
|
+
if (!response.ok) {
|
|
1094
|
+
const body = await response.text();
|
|
1095
|
+
stream.emit("event", {
|
|
1096
|
+
type: "error",
|
|
1097
|
+
error: `SSE chat message stream failed (${response.status}): ${body}`,
|
|
1098
|
+
});
|
|
1099
|
+
stream.close();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const body = await response.text();
|
|
1103
|
+
for (const line of body.split("\n")) {
|
|
1104
|
+
if (!line.startsWith("data:"))
|
|
1105
|
+
continue;
|
|
1106
|
+
const dataStr = line.slice(5).trim();
|
|
1107
|
+
if (!dataStr)
|
|
1108
|
+
continue;
|
|
1109
|
+
try {
|
|
1110
|
+
const eventData = JSON.parse(dataStr);
|
|
1111
|
+
if (eventData.error) {
|
|
1112
|
+
stream.emit("event", {
|
|
1113
|
+
type: "error",
|
|
1114
|
+
error: eventData.error,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
else if (eventData.content && eventData.message_id) {
|
|
1118
|
+
// Done event — has full content + message_id
|
|
1119
|
+
stream.emit("event", {
|
|
1120
|
+
type: "end",
|
|
1121
|
+
messageId: eventData.message_id,
|
|
1122
|
+
executionTimeMs: eventData.execution_time_ms ?? 0,
|
|
1123
|
+
tokenUsage: eventData.token_usage,
|
|
1124
|
+
contextWindow: eventData.context_window,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
else if (eventData.token) {
|
|
1128
|
+
stream.emit("event", {
|
|
1129
|
+
type: "chunk",
|
|
1130
|
+
content: eventData.token,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
// skip malformed SSE data
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
stream.close();
|
|
1139
|
+
}
|
|
1140
|
+
catch (err) {
|
|
1141
|
+
stream.emit("event", {
|
|
1142
|
+
type: "error",
|
|
1143
|
+
error: err.message ?? String(err),
|
|
1144
|
+
});
|
|
1145
|
+
stream.close();
|
|
1146
|
+
}
|
|
1147
|
+
})();
|
|
1148
|
+
return stream;
|
|
1149
|
+
}
|
|
800
1150
|
/**
|
|
801
1151
|
* Get a chat session by ID
|
|
802
1152
|
*/
|
|
@@ -890,6 +1240,14 @@ class EkoDBClient {
|
|
|
890
1240
|
async getChatModels() {
|
|
891
1241
|
return this.makeRequest("GET", "/api/chat_models", undefined, 0, true);
|
|
892
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Get all built-in server-side chat tool definitions.
|
|
1245
|
+
* Returns a list of tool objects with name, description, and parameters fields.
|
|
1246
|
+
* Used by planning agents to discover available tools dynamically.
|
|
1247
|
+
*/
|
|
1248
|
+
async getChatTools() {
|
|
1249
|
+
return this.makeRequest("GET", "/api/chat/tools", undefined, 0, true);
|
|
1250
|
+
}
|
|
893
1251
|
/**
|
|
894
1252
|
* Get available models for a specific provider
|
|
895
1253
|
* @param provider - Provider name (e.g., "openai", "anthropic", "perplexity")
|
|
@@ -993,6 +1351,204 @@ class EkoDBClient {
|
|
|
993
1351
|
await this.makeRequest("DELETE", `/api/functions/${encodeURIComponent(label)}`, undefined, 0, true);
|
|
994
1352
|
}
|
|
995
1353
|
// ========================================================================
|
|
1354
|
+
// GOAL API
|
|
1355
|
+
// ========================================================================
|
|
1356
|
+
/** Create a new goal */
|
|
1357
|
+
async goalCreate(data) {
|
|
1358
|
+
return this.makeRequest("POST", "/api/chat/goals", data, 0, true);
|
|
1359
|
+
}
|
|
1360
|
+
/** List all goals */
|
|
1361
|
+
async goalList() {
|
|
1362
|
+
return this.makeRequest("GET", "/api/chat/goals", undefined, 0, true);
|
|
1363
|
+
}
|
|
1364
|
+
/** Get a goal by ID */
|
|
1365
|
+
async goalGet(id) {
|
|
1366
|
+
return this.makeRequest("GET", `/api/chat/goals/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1367
|
+
}
|
|
1368
|
+
/** Update a goal by ID */
|
|
1369
|
+
async goalUpdate(id, data) {
|
|
1370
|
+
return this.makeRequest("PUT", `/api/chat/goals/${encodeURIComponent(id)}`, data, 0, true);
|
|
1371
|
+
}
|
|
1372
|
+
/** Delete a goal by ID */
|
|
1373
|
+
async goalDelete(id) {
|
|
1374
|
+
await this.makeRequest("DELETE", `/api/chat/goals/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1375
|
+
}
|
|
1376
|
+
// ── Goal Templates ──
|
|
1377
|
+
/** Create a new goal template */
|
|
1378
|
+
async goalTemplateCreate(data) {
|
|
1379
|
+
return this.makeRequest("POST", "/api/chat/goal-templates", data, 0, true);
|
|
1380
|
+
}
|
|
1381
|
+
/** List all goal templates */
|
|
1382
|
+
async goalTemplateList() {
|
|
1383
|
+
return this.makeRequest("GET", "/api/chat/goal-templates", undefined, 0, true);
|
|
1384
|
+
}
|
|
1385
|
+
/** Get a goal template by ID */
|
|
1386
|
+
async goalTemplateGet(id) {
|
|
1387
|
+
return this.makeRequest("GET", `/api/chat/goal-templates/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1388
|
+
}
|
|
1389
|
+
/** Update a goal template by ID */
|
|
1390
|
+
async goalTemplateUpdate(id, data) {
|
|
1391
|
+
return this.makeRequest("PUT", `/api/chat/goal-templates/${encodeURIComponent(id)}`, data, 0, true);
|
|
1392
|
+
}
|
|
1393
|
+
/** Delete a goal template by ID */
|
|
1394
|
+
async goalTemplateDelete(id) {
|
|
1395
|
+
await this.makeRequest("DELETE", `/api/chat/goal-templates/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1396
|
+
}
|
|
1397
|
+
/** Search goals */
|
|
1398
|
+
async goalSearch(query) {
|
|
1399
|
+
const params = new URLSearchParams({ q: query });
|
|
1400
|
+
return this.makeRequest("GET", `/api/chat/goals/search?${params}`, undefined, 0, true);
|
|
1401
|
+
}
|
|
1402
|
+
/** Mark a goal as complete (status -> pending_review) */
|
|
1403
|
+
async goalComplete(id, data) {
|
|
1404
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/complete`, data, 0, true);
|
|
1405
|
+
}
|
|
1406
|
+
/** Approve a goal (status -> in_progress) */
|
|
1407
|
+
async goalApprove(id) {
|
|
1408
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/approve`, undefined, 0, true);
|
|
1409
|
+
}
|
|
1410
|
+
/** Reject a goal (status -> failed) */
|
|
1411
|
+
async goalReject(id, data) {
|
|
1412
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/reject`, data, 0, true);
|
|
1413
|
+
}
|
|
1414
|
+
/** Start a goal step (status -> in_progress) */
|
|
1415
|
+
async goalStepStart(id, stepIndex) {
|
|
1416
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`, undefined, 0, true);
|
|
1417
|
+
}
|
|
1418
|
+
/** Complete a goal step with result */
|
|
1419
|
+
async goalStepComplete(id, stepIndex, data) {
|
|
1420
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`, data, 0, true);
|
|
1421
|
+
}
|
|
1422
|
+
/** Fail a goal step with error */
|
|
1423
|
+
async goalStepFail(id, stepIndex, data) {
|
|
1424
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`, data, 0, true);
|
|
1425
|
+
}
|
|
1426
|
+
// ========================================================================
|
|
1427
|
+
// TASK API
|
|
1428
|
+
// ========================================================================
|
|
1429
|
+
/** Create a new scheduled task */
|
|
1430
|
+
async taskCreate(data) {
|
|
1431
|
+
return this.makeRequest("POST", "/api/chat/tasks", data, 0, true);
|
|
1432
|
+
}
|
|
1433
|
+
/** List all scheduled tasks */
|
|
1434
|
+
async taskList() {
|
|
1435
|
+
return this.makeRequest("GET", "/api/chat/tasks", undefined, 0, true);
|
|
1436
|
+
}
|
|
1437
|
+
/** Get a task by ID */
|
|
1438
|
+
async taskGet(id) {
|
|
1439
|
+
return this.makeRequest("GET", `/api/chat/tasks/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1440
|
+
}
|
|
1441
|
+
/** Update a task by ID */
|
|
1442
|
+
async taskUpdate(id, data) {
|
|
1443
|
+
return this.makeRequest("PUT", `/api/chat/tasks/${encodeURIComponent(id)}`, data, 0, true);
|
|
1444
|
+
}
|
|
1445
|
+
/** Delete a task by ID */
|
|
1446
|
+
async taskDelete(id) {
|
|
1447
|
+
await this.makeRequest("DELETE", `/api/chat/tasks/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1448
|
+
}
|
|
1449
|
+
/** Get tasks that are due at the given time */
|
|
1450
|
+
async taskDue(now) {
|
|
1451
|
+
const params = new URLSearchParams({ now });
|
|
1452
|
+
return this.makeRequest("GET", `/api/chat/tasks/due?${params}`, undefined, 0, true);
|
|
1453
|
+
}
|
|
1454
|
+
/** Start a task (status -> running) */
|
|
1455
|
+
async taskStart(id) {
|
|
1456
|
+
return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/start`, undefined, 0, true);
|
|
1457
|
+
}
|
|
1458
|
+
/** Mark a task as succeeded */
|
|
1459
|
+
async taskSucceed(id, data) {
|
|
1460
|
+
return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/succeed`, data, 0, true);
|
|
1461
|
+
}
|
|
1462
|
+
/** Mark a task as failed */
|
|
1463
|
+
async taskFail(id, data) {
|
|
1464
|
+
return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/fail`, data, 0, true);
|
|
1465
|
+
}
|
|
1466
|
+
/** Pause a task */
|
|
1467
|
+
async taskPause(id) {
|
|
1468
|
+
return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/pause`, undefined, 0, true);
|
|
1469
|
+
}
|
|
1470
|
+
/** Resume a paused task */
|
|
1471
|
+
async taskResume(id, data) {
|
|
1472
|
+
return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/resume`, data, 0, true);
|
|
1473
|
+
}
|
|
1474
|
+
// ========================================================================
|
|
1475
|
+
// AGENT API
|
|
1476
|
+
// ========================================================================
|
|
1477
|
+
/** Create a new agent */
|
|
1478
|
+
async agentCreate(data) {
|
|
1479
|
+
return this.makeRequest("POST", "/api/chat/agents", data, 0, true);
|
|
1480
|
+
}
|
|
1481
|
+
/** List all agents */
|
|
1482
|
+
async agentList() {
|
|
1483
|
+
return this.makeRequest("GET", "/api/chat/agents", undefined, 0, true);
|
|
1484
|
+
}
|
|
1485
|
+
/** Get an agent by ID */
|
|
1486
|
+
async agentGet(id) {
|
|
1487
|
+
return this.makeRequest("GET", `/api/chat/agents/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1488
|
+
}
|
|
1489
|
+
/** Get an agent by name */
|
|
1490
|
+
async agentGetByName(name) {
|
|
1491
|
+
return this.makeRequest("GET", `/api/chat/agents/by-name/${encodeURIComponent(name)}`, undefined, 0, true);
|
|
1492
|
+
}
|
|
1493
|
+
/** Update an agent by ID */
|
|
1494
|
+
async agentUpdate(id, data) {
|
|
1495
|
+
return this.makeRequest("PUT", `/api/chat/agents/${encodeURIComponent(id)}`, data, 0, true);
|
|
1496
|
+
}
|
|
1497
|
+
/** Delete an agent by ID */
|
|
1498
|
+
async agentDelete(id) {
|
|
1499
|
+
await this.makeRequest("DELETE", `/api/chat/agents/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1500
|
+
}
|
|
1501
|
+
/** Get agents by deployment ID */
|
|
1502
|
+
async agentsByDeployment(deploymentId) {
|
|
1503
|
+
return this.makeRequest("GET", `/api/chat/agents/by-deployment/${encodeURIComponent(deploymentId)}`, undefined, 0, true);
|
|
1504
|
+
}
|
|
1505
|
+
// ========================================================================
|
|
1506
|
+
// KV DOCUMENT LINKING
|
|
1507
|
+
// ========================================================================
|
|
1508
|
+
/** Get documents linked to a KV key */
|
|
1509
|
+
async kvGetLinks(key) {
|
|
1510
|
+
return this.makeRequest("GET", `/api/kv/links/${encodeURIComponent(key)}`, undefined, 0, true);
|
|
1511
|
+
}
|
|
1512
|
+
/** Link a document to a KV key */
|
|
1513
|
+
async kvLink(key, collection, documentId) {
|
|
1514
|
+
return this.makeRequest("POST", `/api/kv/link`, { key, collection, document_id: documentId }, 0, true);
|
|
1515
|
+
}
|
|
1516
|
+
/** Unlink a document from a KV key */
|
|
1517
|
+
async kvUnlink(key, collection, documentId) {
|
|
1518
|
+
return this.makeRequest("POST", `/api/kv/unlink`, { key, collection, document_id: documentId }, 0, true);
|
|
1519
|
+
}
|
|
1520
|
+
// ========================================================================
|
|
1521
|
+
// SCHEDULE MANAGEMENT
|
|
1522
|
+
// ========================================================================
|
|
1523
|
+
/** Create a new schedule */
|
|
1524
|
+
async createSchedule(data) {
|
|
1525
|
+
return this.makeRequest("POST", `/api/schedules`, data, 0, true);
|
|
1526
|
+
}
|
|
1527
|
+
/** List all schedules */
|
|
1528
|
+
async listSchedules() {
|
|
1529
|
+
return this.makeRequest("GET", `/api/schedules`, undefined, 0, true);
|
|
1530
|
+
}
|
|
1531
|
+
/** Get a schedule by ID */
|
|
1532
|
+
async getSchedule(id) {
|
|
1533
|
+
return this.makeRequest("GET", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1534
|
+
}
|
|
1535
|
+
/** Update a schedule */
|
|
1536
|
+
async updateSchedule(id, data) {
|
|
1537
|
+
return this.makeRequest("PUT", `/api/schedules/${encodeURIComponent(id)}`, data, 0, true);
|
|
1538
|
+
}
|
|
1539
|
+
/** Delete a schedule */
|
|
1540
|
+
async deleteSchedule(id) {
|
|
1541
|
+
await this.makeRequest("DELETE", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1542
|
+
}
|
|
1543
|
+
/** Pause a schedule */
|
|
1544
|
+
async pauseSchedule(id) {
|
|
1545
|
+
return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/pause`, undefined, 0, true);
|
|
1546
|
+
}
|
|
1547
|
+
/** Resume a schedule */
|
|
1548
|
+
async resumeSchedule(id) {
|
|
1549
|
+
return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/resume`, undefined, 0, true);
|
|
1550
|
+
}
|
|
1551
|
+
// ========================================================================
|
|
996
1552
|
// COLLECTION UTILITIES
|
|
997
1553
|
// ========================================================================
|
|
998
1554
|
/**
|
|
@@ -1150,22 +1706,64 @@ class EkoDBClient {
|
|
|
1150
1706
|
}
|
|
1151
1707
|
}
|
|
1152
1708
|
exports.EkoDBClient = EkoDBClient;
|
|
1709
|
+
/** EventEmitter-like interface for subscriptions and chat streams. */
|
|
1710
|
+
class EventStream {
|
|
1711
|
+
constructor() {
|
|
1712
|
+
this.listeners = new Map();
|
|
1713
|
+
this._closed = false;
|
|
1714
|
+
}
|
|
1715
|
+
on(event, listener) {
|
|
1716
|
+
if (!this.listeners.has(event)) {
|
|
1717
|
+
this.listeners.set(event, []);
|
|
1718
|
+
}
|
|
1719
|
+
this.listeners.get(event).push(listener);
|
|
1720
|
+
return this;
|
|
1721
|
+
}
|
|
1722
|
+
/** @internal */
|
|
1723
|
+
emit(event, data) {
|
|
1724
|
+
const handlers = this.listeners.get(event);
|
|
1725
|
+
if (handlers) {
|
|
1726
|
+
for (const handler of handlers) {
|
|
1727
|
+
handler(data);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
get closed() {
|
|
1732
|
+
return this._closed;
|
|
1733
|
+
}
|
|
1734
|
+
/** @internal */
|
|
1735
|
+
close() {
|
|
1736
|
+
this._closed = true;
|
|
1737
|
+
this.emit("close");
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
exports.EventStream = EventStream;
|
|
1153
1741
|
/**
|
|
1154
|
-
* WebSocket client for real-time queries
|
|
1742
|
+
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1155
1743
|
*/
|
|
1156
1744
|
class WebSocketClient {
|
|
1157
1745
|
constructor(wsURL, token) {
|
|
1158
1746
|
this.ws = null;
|
|
1747
|
+
this.dispatcherRunning = false;
|
|
1748
|
+
// Dispatcher state
|
|
1749
|
+
this.pendingRequests = new Map();
|
|
1750
|
+
this.subscriptions = new Map();
|
|
1751
|
+
this.chatStreams = new Map();
|
|
1752
|
+
this.registerToolsAck = null;
|
|
1753
|
+
this.messageCounter = 0;
|
|
1159
1754
|
this.wsURL = wsURL;
|
|
1160
1755
|
this.token = token;
|
|
1161
1756
|
}
|
|
1757
|
+
genMessageId() {
|
|
1758
|
+
const counter = this.messageCounter++;
|
|
1759
|
+
return `${Date.now()}-${counter}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1760
|
+
}
|
|
1162
1761
|
/**
|
|
1163
|
-
* Connect
|
|
1762
|
+
* Connect and start the dispatcher.
|
|
1164
1763
|
*/
|
|
1165
|
-
async
|
|
1166
|
-
if (this.ws)
|
|
1764
|
+
async ensureConnected() {
|
|
1765
|
+
if (this.ws && this.dispatcherRunning)
|
|
1167
1766
|
return;
|
|
1168
|
-
// Dynamic import for Node.js WebSocket
|
|
1169
1767
|
const WebSocket = (await Promise.resolve().then(() => __importStar(require("ws")))).default;
|
|
1170
1768
|
let url = this.wsURL;
|
|
1171
1769
|
if (!url.endsWith("/api/ws")) {
|
|
@@ -1176,43 +1774,323 @@ class WebSocketClient {
|
|
|
1176
1774
|
Authorization: `Bearer ${this.token}`,
|
|
1177
1775
|
},
|
|
1178
1776
|
});
|
|
1179
|
-
|
|
1777
|
+
await new Promise((resolve, reject) => {
|
|
1180
1778
|
this.ws.on("open", () => resolve());
|
|
1181
1779
|
this.ws.on("error", (err) => reject(err));
|
|
1182
1780
|
});
|
|
1781
|
+
this.spawnDispatcher();
|
|
1782
|
+
}
|
|
1783
|
+
spawnDispatcher() {
|
|
1784
|
+
if (this.dispatcherRunning)
|
|
1785
|
+
return;
|
|
1786
|
+
this.dispatcherRunning = true;
|
|
1787
|
+
this.ws.on("message", (data) => {
|
|
1788
|
+
try {
|
|
1789
|
+
const msg = JSON.parse(data.toString());
|
|
1790
|
+
this.routeMessage(msg);
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
// Ignore malformed messages
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
this.ws.on("close", () => {
|
|
1797
|
+
this.dispatcherRunning = false;
|
|
1798
|
+
// Notify all pending requests
|
|
1799
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1800
|
+
pending.reject(new Error("WebSocket connection closed"));
|
|
1801
|
+
}
|
|
1802
|
+
this.pendingRequests.clear();
|
|
1803
|
+
// Close all chat streams
|
|
1804
|
+
for (const [, stream] of this.chatStreams) {
|
|
1805
|
+
stream.emit("event", { type: "error", error: "Connection closed" });
|
|
1806
|
+
stream.close();
|
|
1807
|
+
}
|
|
1808
|
+
this.chatStreams.clear();
|
|
1809
|
+
// Close all subscriptions
|
|
1810
|
+
for (const [, stream] of this.subscriptions) {
|
|
1811
|
+
stream.close();
|
|
1812
|
+
}
|
|
1813
|
+
this.subscriptions.clear();
|
|
1814
|
+
this.ws = null;
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
routeMessage(msg) {
|
|
1818
|
+
switch (msg.type) {
|
|
1819
|
+
case "Success":
|
|
1820
|
+
case "Error": {
|
|
1821
|
+
// Try messageId from top-level, then from payload
|
|
1822
|
+
const messageId = msg.messageId ||
|
|
1823
|
+
msg.message_id ||
|
|
1824
|
+
msg.payload?.message_id ||
|
|
1825
|
+
msg.payload?.messageId;
|
|
1826
|
+
let matched = false;
|
|
1827
|
+
if (messageId && this.pendingRequests.has(messageId)) {
|
|
1828
|
+
const pending = this.pendingRequests.get(messageId);
|
|
1829
|
+
this.pendingRequests.delete(messageId);
|
|
1830
|
+
if (msg.type === "Error") {
|
|
1831
|
+
pending.reject(new Error(msg.message || "Unknown error"));
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
pending.resolve(msg.payload);
|
|
1835
|
+
}
|
|
1836
|
+
matched = true;
|
|
1837
|
+
}
|
|
1838
|
+
if (!matched && this.registerToolsAck) {
|
|
1839
|
+
const ack = this.registerToolsAck;
|
|
1840
|
+
this.registerToolsAck = null;
|
|
1841
|
+
if (msg.type === "Error") {
|
|
1842
|
+
ack.reject(new Error(msg.message || "Tool registration failed"));
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
ack.resolve(msg.payload);
|
|
1846
|
+
}
|
|
1847
|
+
matched = true;
|
|
1848
|
+
}
|
|
1849
|
+
// Server doesn't echo messageId — if there's exactly one pending
|
|
1850
|
+
// request, deliver the response to it (sequential request/response).
|
|
1851
|
+
if (!matched && this.pendingRequests.size === 1) {
|
|
1852
|
+
const entry = this.pendingRequests.entries().next().value;
|
|
1853
|
+
const key = entry[0];
|
|
1854
|
+
const pending = entry[1];
|
|
1855
|
+
this.pendingRequests.delete(key);
|
|
1856
|
+
if (msg.type === "Error") {
|
|
1857
|
+
pending.reject(new Error(msg.message || "Unknown error"));
|
|
1858
|
+
}
|
|
1859
|
+
else {
|
|
1860
|
+
pending.resolve(msg.payload);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
case "MutationNotification": {
|
|
1866
|
+
const payload = msg.payload;
|
|
1867
|
+
const notification = {
|
|
1868
|
+
collection: payload.collection,
|
|
1869
|
+
event: payload.event,
|
|
1870
|
+
recordIds: payload.record_ids || payload.recordIds || [],
|
|
1871
|
+
records: payload.records,
|
|
1872
|
+
timestamp: payload.timestamp,
|
|
1873
|
+
};
|
|
1874
|
+
for (const [collection, stream] of this.subscriptions) {
|
|
1875
|
+
if (collection === notification.collection) {
|
|
1876
|
+
stream.emit("mutation", notification);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
case "ChatStreamChunk": {
|
|
1882
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1883
|
+
const stream = this.chatStreams.get(chatId);
|
|
1884
|
+
if (stream) {
|
|
1885
|
+
stream.emit("event", {
|
|
1886
|
+
type: "chunk",
|
|
1887
|
+
content: msg.payload.content,
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
case "ChatStreamEnd": {
|
|
1893
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1894
|
+
const stream = this.chatStreams.get(chatId);
|
|
1895
|
+
if (stream) {
|
|
1896
|
+
stream.emit("event", {
|
|
1897
|
+
type: "end",
|
|
1898
|
+
messageId: msg.payload.message_id || msg.payload.messageId || "",
|
|
1899
|
+
tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
|
|
1900
|
+
toolCallHistory: msg.payload.tool_call_history || msg.payload.toolCallHistory,
|
|
1901
|
+
executionTimeMs: msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
|
|
1902
|
+
contextWindow: msg.payload.context_window || msg.payload.contextWindow,
|
|
1903
|
+
});
|
|
1904
|
+
this.chatStreams.delete(chatId);
|
|
1905
|
+
stream.close();
|
|
1906
|
+
}
|
|
1907
|
+
break;
|
|
1908
|
+
}
|
|
1909
|
+
case "ChatStreamError": {
|
|
1910
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1911
|
+
const stream = this.chatStreams.get(chatId);
|
|
1912
|
+
if (stream) {
|
|
1913
|
+
stream.emit("event", {
|
|
1914
|
+
type: "error",
|
|
1915
|
+
error: msg.payload.error || msg.payload.message || "Unknown error",
|
|
1916
|
+
});
|
|
1917
|
+
this.chatStreams.delete(chatId);
|
|
1918
|
+
stream.close();
|
|
1919
|
+
}
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
case "ClientToolCall": {
|
|
1923
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1924
|
+
const stream = this.chatStreams.get(chatId);
|
|
1925
|
+
if (stream) {
|
|
1926
|
+
stream.emit("event", {
|
|
1927
|
+
type: "toolCall",
|
|
1928
|
+
chatId,
|
|
1929
|
+
callId: msg.payload.call_id || msg.payload.callId,
|
|
1930
|
+
toolName: msg.payload.tool_name || msg.payload.toolName,
|
|
1931
|
+
arguments: msg.payload.arguments,
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
async sendRequest(request) {
|
|
1939
|
+
await this.ensureConnected();
|
|
1940
|
+
const messageId = request.messageId || request.message_id;
|
|
1941
|
+
return new Promise((resolve, reject) => {
|
|
1942
|
+
this.pendingRequests.set(messageId, { resolve, reject });
|
|
1943
|
+
try {
|
|
1944
|
+
this.ws.send(JSON.stringify(request));
|
|
1945
|
+
}
|
|
1946
|
+
catch (err) {
|
|
1947
|
+
this.pendingRequests.delete(messageId);
|
|
1948
|
+
reject(err);
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1183
1951
|
}
|
|
1184
1952
|
/**
|
|
1185
|
-
* Find all records in a collection via WebSocket
|
|
1953
|
+
* Find all records in a collection via WebSocket.
|
|
1186
1954
|
*/
|
|
1187
1955
|
async findAll(collection) {
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
const request = {
|
|
1956
|
+
const messageId = this.genMessageId();
|
|
1957
|
+
const payload = await this.sendRequest({
|
|
1191
1958
|
type: "FindAll",
|
|
1192
1959
|
messageId,
|
|
1193
1960
|
payload: { collection },
|
|
1961
|
+
});
|
|
1962
|
+
return payload?.data || [];
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Subscribe to mutation notifications on a collection.
|
|
1966
|
+
* Returns an EventStream that emits "mutation" events.
|
|
1967
|
+
*/
|
|
1968
|
+
async subscribe(collection, options) {
|
|
1969
|
+
await this.ensureConnected();
|
|
1970
|
+
if (this.subscriptions.has(collection)) {
|
|
1971
|
+
throw new Error(`Already subscribed to collection "${collection}"`);
|
|
1972
|
+
}
|
|
1973
|
+
const messageId = this.genMessageId();
|
|
1974
|
+
const stream = new EventStream();
|
|
1975
|
+
this.subscriptions.set(collection, stream);
|
|
1976
|
+
const request = {
|
|
1977
|
+
type: "Subscribe",
|
|
1978
|
+
messageId,
|
|
1979
|
+
payload: {
|
|
1980
|
+
collection,
|
|
1981
|
+
...(options?.filterField && { filter_field: options.filterField }),
|
|
1982
|
+
...(options?.filterValue && { filter_value: options.filterValue }),
|
|
1983
|
+
},
|
|
1194
1984
|
};
|
|
1195
|
-
|
|
1985
|
+
// Send subscribe request and wait for ack
|
|
1986
|
+
try {
|
|
1987
|
+
await this.sendRequest(request);
|
|
1988
|
+
}
|
|
1989
|
+
catch (err) {
|
|
1990
|
+
this.subscriptions.delete(collection);
|
|
1991
|
+
throw err;
|
|
1992
|
+
}
|
|
1993
|
+
return stream;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Send a chat message and receive a streaming response.
|
|
1997
|
+
* Returns an EventStream that emits "event" with ChatStreamEvent objects.
|
|
1998
|
+
*/
|
|
1999
|
+
async chatSend(chatId, message, options) {
|
|
2000
|
+
await this.ensureConnected();
|
|
2001
|
+
if (this.chatStreams.has(chatId)) {
|
|
2002
|
+
throw new Error(`Chat stream already active for chatId "${chatId}"`);
|
|
2003
|
+
}
|
|
2004
|
+
const stream = new EventStream();
|
|
2005
|
+
this.chatStreams.set(chatId, stream);
|
|
2006
|
+
const request = {
|
|
2007
|
+
type: "ChatSend",
|
|
2008
|
+
payload: {
|
|
2009
|
+
chat_id: chatId,
|
|
2010
|
+
message,
|
|
2011
|
+
...(options?.bypassRipple != null && {
|
|
2012
|
+
bypass_ripple: options.bypassRipple,
|
|
2013
|
+
}),
|
|
2014
|
+
...(options?.clientTools && { client_tools: options.clientTools }),
|
|
2015
|
+
...(options?.maxIterations != null && {
|
|
2016
|
+
max_iterations: options.maxIterations,
|
|
2017
|
+
}),
|
|
2018
|
+
...(options?.confirmTools && { confirm_tools: options.confirmTools }),
|
|
2019
|
+
...(options?.excludeTools && { exclude_tools: options.excludeTools }),
|
|
2020
|
+
},
|
|
2021
|
+
};
|
|
2022
|
+
this.ws.send(JSON.stringify(request));
|
|
2023
|
+
return stream;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Register client-side tools for a chat session.
|
|
2027
|
+
*/
|
|
2028
|
+
async registerClientTools(chatId, tools) {
|
|
2029
|
+
await this.ensureConnected();
|
|
2030
|
+
const request = {
|
|
2031
|
+
type: "RegisterClientTools",
|
|
2032
|
+
payload: {
|
|
2033
|
+
chat_id: chatId,
|
|
2034
|
+
tools,
|
|
2035
|
+
},
|
|
2036
|
+
};
|
|
2037
|
+
await new Promise((resolve, reject) => {
|
|
2038
|
+
this.registerToolsAck = {
|
|
2039
|
+
resolve: () => resolve(),
|
|
2040
|
+
reject: (err) => reject(err),
|
|
2041
|
+
};
|
|
1196
2042
|
this.ws.send(JSON.stringify(request));
|
|
1197
|
-
this.ws.once("message", (data) => {
|
|
1198
|
-
const response = JSON.parse(data.toString());
|
|
1199
|
-
if (response.type === "Error") {
|
|
1200
|
-
reject(new Error(response.message));
|
|
1201
|
-
}
|
|
1202
|
-
else {
|
|
1203
|
-
resolve(response.payload?.data || []);
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1206
|
-
this.ws.once("error", reject);
|
|
1207
2043
|
});
|
|
1208
2044
|
}
|
|
1209
2045
|
/**
|
|
1210
|
-
*
|
|
2046
|
+
* Send a tool result back to the server during a chat stream.
|
|
2047
|
+
*/
|
|
2048
|
+
async sendToolResult(chatId, callId, success, result, error) {
|
|
2049
|
+
await this.ensureConnected();
|
|
2050
|
+
const request = {
|
|
2051
|
+
type: "ClientToolResult",
|
|
2052
|
+
payload: {
|
|
2053
|
+
chat_id: chatId,
|
|
2054
|
+
call_id: callId,
|
|
2055
|
+
success,
|
|
2056
|
+
...(result !== undefined && { result }),
|
|
2057
|
+
...(error !== undefined && { error }),
|
|
2058
|
+
},
|
|
2059
|
+
};
|
|
2060
|
+
this.ws.send(JSON.stringify(request));
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Stateless raw LLM completion via WebSocket.
|
|
2064
|
+
*
|
|
2065
|
+
* Sends a RawComplete message and waits for the Success response.
|
|
2066
|
+
* Preferred over HTTP for deployed instances: the persistent WSS
|
|
2067
|
+
* connection is already authenticated and won't be killed by reverse
|
|
2068
|
+
* proxy timeouts.
|
|
2069
|
+
*/
|
|
2070
|
+
async rawCompletion(request) {
|
|
2071
|
+
await this.ensureConnected();
|
|
2072
|
+
const messageId = this.genMessageId();
|
|
2073
|
+
const payload = await this.sendRequest({
|
|
2074
|
+
type: "RawComplete",
|
|
2075
|
+
messageId,
|
|
2076
|
+
payload: {
|
|
2077
|
+
system_prompt: request.system_prompt,
|
|
2078
|
+
message: request.message,
|
|
2079
|
+
...(request.provider && { provider: request.provider }),
|
|
2080
|
+
...(request.model && { model: request.model }),
|
|
2081
|
+
...(request.max_tokens != null && { max_tokens: request.max_tokens }),
|
|
2082
|
+
},
|
|
2083
|
+
});
|
|
2084
|
+
return { content: payload?.data?.content || "" };
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Close the WebSocket connection.
|
|
1211
2088
|
*/
|
|
1212
2089
|
close() {
|
|
1213
2090
|
if (this.ws) {
|
|
1214
2091
|
this.ws.close();
|
|
1215
2092
|
this.ws = null;
|
|
2093
|
+
this.dispatcherRunning = false;
|
|
1216
2094
|
}
|
|
1217
2095
|
}
|
|
1218
2096
|
}
|