@ekodb/ekodb-client 0.15.2 → 0.17.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 CHANGED
@@ -4,7 +4,7 @@
4
4
  import { QueryBuilder } from "./query-builder";
5
5
  import { SearchQuery, SearchResponse } from "./search";
6
6
  import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
7
- import { Script, FunctionResult } from "./functions";
7
+ import { UserFunction, FunctionResult } from "./functions";
8
8
  export interface Record {
9
9
  [key: string]: any;
10
10
  }
@@ -156,6 +156,7 @@ export interface CreateChatSessionRequest {
156
156
  llm_provider: string;
157
157
  llm_model?: string;
158
158
  system_prompt?: string;
159
+ agent_id?: string;
159
160
  bypass_ripple?: boolean;
160
161
  parent_id?: string;
161
162
  branch_point_idx?: number;
@@ -171,6 +172,9 @@ export interface ChatMessageRequest {
171
172
  max_iterations?: number;
172
173
  tool_config?: ToolConfig;
173
174
  llm_model?: string;
175
+ client_tools?: ClientToolDefinition[];
176
+ confirm_tools?: string[];
177
+ exclude_tools?: string[];
174
178
  }
175
179
  export interface TokenUsage {
176
180
  prompt_tokens: number;
@@ -193,6 +197,7 @@ export interface ChatSession {
193
197
  llm_model: string;
194
198
  collections: CollectionConfig[];
195
199
  system_prompt?: string;
200
+ agent_id?: string;
196
201
  title?: string;
197
202
  message_count: number;
198
203
  }
@@ -285,23 +290,6 @@ export interface RawCompletionRequest {
285
290
  export interface RawCompletionResponse {
286
291
  content: string;
287
292
  }
288
- /**
289
- * User function definition - reusable sequence of Functions that can be called by Scripts
290
- */
291
- export interface UserFunction {
292
- label: string;
293
- name: string;
294
- description?: string;
295
- version?: string;
296
- parameters: {
297
- [key: string]: ParameterDefinition;
298
- };
299
- functions: FunctionStageConfig[];
300
- tags?: string[];
301
- id?: string;
302
- created_at?: string;
303
- updated_at?: string;
304
- }
305
293
  /**
306
294
  * Parameter definition for functions
307
295
  */
@@ -821,6 +809,11 @@ export declare class EkoDBClient {
821
809
  * Send a message in an existing chat session
822
810
  */
823
811
  chatMessage(sessionId: string, request: ChatMessageRequest): Promise<ChatResponse>;
812
+ /**
813
+ * Submit a client tool result for an in-flight SSE chat stream.
814
+ * Unblocks ekoDB's tool loop so it can feed the result to the LLM.
815
+ */
816
+ submitChatToolResult(chatId: string, callId: string, success: boolean, result?: any, error?: string): Promise<void>;
824
817
  /**
825
818
  * Send a message in an existing chat session via SSE streaming.
826
819
  *
@@ -902,29 +895,29 @@ export declare class EkoDBClient {
902
895
  */
903
896
  getChatMessage(sessionId: string, messageId: string): Promise<Record>;
904
897
  /**
905
- * Save a new script definition
898
+ * Save a new function definition
906
899
  */
907
- saveScript(script: Script): Promise<string>;
900
+ saveFunction(script: UserFunction): Promise<string>;
908
901
  /**
909
- * Get a script by ID
902
+ * Get a function by ID
910
903
  */
911
- getScript(id: string): Promise<Script>;
904
+ getFunction(id: string): Promise<UserFunction>;
912
905
  /**
913
- * List all scripts, optionally filtered by tags
906
+ * List all functions, optionally filtered by tags
914
907
  */
915
- listScripts(tags?: string[]): Promise<Script[]>;
908
+ listFunctions(tags?: string[]): Promise<UserFunction[]>;
916
909
  /**
917
- * Update an existing script by ID
910
+ * Update an existing function by ID
918
911
  */
919
- updateScript(id: string, script: Script): Promise<void>;
912
+ updateFunction(id: string, script: UserFunction): Promise<void>;
920
913
  /**
921
- * Delete a script by ID
914
+ * Delete a function by ID
922
915
  */
923
- deleteScript(id: string): Promise<void>;
916
+ deleteFunction(id: string): Promise<void>;
924
917
  /**
925
- * Call a saved script by ID or label
918
+ * Call a saved function by ID or label
926
919
  */
927
- callScript(idOrLabel: string, params?: {
920
+ callFunction(idOrLabel: string, params?: {
928
921
  [key: string]: any;
929
922
  }): Promise<FunctionResult>;
930
923
  /**
@@ -1058,6 +1051,17 @@ export declare class EkoDBClient {
1058
1051
  * @returns Number of documents in the collection
1059
1052
  */
1060
1053
  countDocuments(collection: string): Promise<number>;
1054
+ /**
1055
+ * Subscribe to collection mutations via SSE (Server-Sent Events).
1056
+ *
1057
+ * Returns an EventStream that emits MutationNotification events.
1058
+ * Use this when WebSocket connections aren't available (e.g. behind
1059
+ * reverse proxies that block WS upgrades).
1060
+ */
1061
+ subscribeSSE(collection: string, options?: {
1062
+ filterField?: string;
1063
+ filterValue?: string;
1064
+ }): EventStream<MutationNotification>;
1061
1065
  /**
1062
1066
  * Create a WebSocket client
1063
1067
  */
package/dist/client.js CHANGED
@@ -1098,6 +1098,18 @@ class EkoDBClient {
1098
1098
  async chatMessage(sessionId, request) {
1099
1099
  return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
1100
1100
  }
1101
+ /**
1102
+ * Submit a client tool result for an in-flight SSE chat stream.
1103
+ * Unblocks ekoDB's tool loop so it can feed the result to the LLM.
1104
+ */
1105
+ async submitChatToolResult(chatId, callId, success, result, error) {
1106
+ await this.makeRequest("POST", `/api/chat/${chatId}/tool-result`, {
1107
+ call_id: callId,
1108
+ success,
1109
+ ...(result !== undefined && { result }),
1110
+ ...(error !== undefined && { error }),
1111
+ }, 0, true);
1112
+ }
1101
1113
  /**
1102
1114
  * Send a message in an existing chat session via SSE streaming.
1103
1115
  *
@@ -1307,41 +1319,41 @@ class EkoDBClient {
1307
1319
  // SCRIPTS API
1308
1320
  // ========================================================================
1309
1321
  /**
1310
- * Save a new script definition
1322
+ * Save a new function definition
1311
1323
  */
1312
- async saveScript(script) {
1324
+ async saveFunction(script) {
1313
1325
  const result = await this.makeRequest("POST", "/api/functions", script);
1314
1326
  return result.id;
1315
1327
  }
1316
1328
  /**
1317
- * Get a script by ID
1329
+ * Get a function by ID
1318
1330
  */
1319
- async getScript(id) {
1331
+ async getFunction(id) {
1320
1332
  return this.makeRequest("GET", `/api/functions/${id}`);
1321
1333
  }
1322
1334
  /**
1323
- * List all scripts, optionally filtered by tags
1335
+ * List all functions, optionally filtered by tags
1324
1336
  */
1325
- async listScripts(tags) {
1337
+ async listFunctions(tags) {
1326
1338
  const params = tags ? `?tags=${tags.join(",")}` : "";
1327
1339
  return this.makeRequest("GET", `/api/functions${params}`);
1328
1340
  }
1329
1341
  /**
1330
- * Update an existing script by ID
1342
+ * Update an existing function by ID
1331
1343
  */
1332
- async updateScript(id, script) {
1344
+ async updateFunction(id, script) {
1333
1345
  await this.makeRequest("PUT", `/api/functions/${id}`, script);
1334
1346
  }
1335
1347
  /**
1336
- * Delete a script by ID
1348
+ * Delete a function by ID
1337
1349
  */
1338
- async deleteScript(id) {
1350
+ async deleteFunction(id) {
1339
1351
  await this.makeRequest("DELETE", `/api/functions/${id}`);
1340
1352
  }
1341
1353
  /**
1342
- * Call a saved script by ID or label
1354
+ * Call a saved function by ID or label
1343
1355
  */
1344
- async callScript(idOrLabel, params) {
1356
+ async callFunction(idOrLabel, params) {
1345
1357
  return this.makeRequest("POST", `/api/functions/${idOrLabel}`, params || {});
1346
1358
  }
1347
1359
  // ========================================================================
@@ -1613,6 +1625,99 @@ class EkoDBClient {
1613
1625
  const records = await this.find(collection, query);
1614
1626
  return records.length;
1615
1627
  }
1628
+ /**
1629
+ * Subscribe to collection mutations via SSE (Server-Sent Events).
1630
+ *
1631
+ * Returns an EventStream that emits MutationNotification events.
1632
+ * Use this when WebSocket connections aren't available (e.g. behind
1633
+ * reverse proxies that block WS upgrades).
1634
+ */
1635
+ subscribeSSE(collection, options) {
1636
+ const stream = new EventStream();
1637
+ (async () => {
1638
+ try {
1639
+ let token = await this.getToken();
1640
+ if (!token) {
1641
+ await this.refreshToken();
1642
+ token = await this.getToken();
1643
+ }
1644
+ let url = `${this.baseURL}/api/subscribe/${encodeURIComponent(collection)}`;
1645
+ const params = [];
1646
+ if (options?.filterField)
1647
+ params.push(`filter_field=${encodeURIComponent(options.filterField)}`);
1648
+ if (options?.filterValue)
1649
+ params.push(`filter_value=${encodeURIComponent(options.filterValue)}`);
1650
+ if (params.length > 0)
1651
+ url += `?${params.join("&")}`;
1652
+ const response = await fetch(url, {
1653
+ method: "GET",
1654
+ headers: {
1655
+ Accept: "text/event-stream",
1656
+ Authorization: `Bearer ${token}`,
1657
+ },
1658
+ });
1659
+ if (!response.ok) {
1660
+ const body = await response.text();
1661
+ stream.emit("error", `SSE subscribe failed (${response.status}): ${body}`);
1662
+ stream.close();
1663
+ return;
1664
+ }
1665
+ const reader = response.body?.getReader();
1666
+ if (!reader) {
1667
+ stream.emit("error", "SSE subscribe failed: streaming not supported");
1668
+ stream.close();
1669
+ return;
1670
+ }
1671
+ const decoder = new TextDecoder("utf-8");
1672
+ let buffer = "";
1673
+ let eventType = "";
1674
+ let dataLines = [];
1675
+ while (!stream.closed) {
1676
+ const { value, done } = await reader.read();
1677
+ if (done)
1678
+ break;
1679
+ buffer += decoder.decode(value, { stream: true });
1680
+ const lines = buffer.split("\n");
1681
+ buffer = lines.pop() ?? "";
1682
+ for (const line of lines) {
1683
+ if (line === "") {
1684
+ // End of event block
1685
+ if (eventType === "mutation" && dataLines.length > 0) {
1686
+ try {
1687
+ const payload = JSON.parse(dataLines.join("\n"));
1688
+ stream.emit("event", {
1689
+ collection: payload.collection,
1690
+ event: payload.event,
1691
+ recordIds: payload.record_ids,
1692
+ records: payload.records,
1693
+ timestamp: payload.timestamp,
1694
+ });
1695
+ }
1696
+ catch {
1697
+ // skip malformed data
1698
+ }
1699
+ }
1700
+ eventType = "";
1701
+ dataLines = [];
1702
+ continue;
1703
+ }
1704
+ if (line.startsWith("event: ")) {
1705
+ eventType = line.slice(7).trim();
1706
+ }
1707
+ else if (line.startsWith("data: ")) {
1708
+ dataLines.push(line.slice(6).trim());
1709
+ }
1710
+ }
1711
+ }
1712
+ stream.close();
1713
+ }
1714
+ catch (err) {
1715
+ stream.emit("error", err.message ?? String(err));
1716
+ stream.close();
1717
+ }
1718
+ })();
1719
+ return stream;
1720
+ }
1616
1721
  /**
1617
1722
  * Create a WebSocket client
1618
1723
  */
@@ -590,14 +590,14 @@ function mockErrorResponse(status, message) {
590
590
  parameters: {},
591
591
  functions: [],
592
592
  };
593
- const result = await client.saveScript(script);
593
+ const result = await client.saveFunction(script);
594
594
  (0, vitest_1.expect)(result).toBeDefined();
595
595
  });
596
596
  (0, vitest_1.it)("gets script by ID", async () => {
597
597
  const client = createTestClient();
598
598
  mockTokenResponse();
599
599
  mockJsonResponse({ id: "func_123", label: "my_function" });
600
- const result = await client.getScript("func_123");
600
+ const result = await client.getFunction("func_123");
601
601
  (0, vitest_1.expect)(result).toBeDefined();
602
602
  });
603
603
  (0, vitest_1.it)("updates script", async () => {
@@ -610,7 +610,7 @@ function mockErrorResponse(status, message) {
610
610
  parameters: {},
611
611
  functions: [],
612
612
  };
613
- await (0, vitest_1.expect)(client.updateScript("func_123", script)).resolves.not.toThrow();
613
+ await (0, vitest_1.expect)(client.updateFunction("func_123", script)).resolves.not.toThrow();
614
614
  });
615
615
  });
616
616
  (0, vitest_1.describe)("EkoDBClient chat advanced", () => {
@@ -2023,3 +2023,204 @@ function mockErrorResponse(status, message) {
2023
2023
  (0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
2024
2024
  });
2025
2025
  });
2026
+ // ============================================================================
2027
+ // agent_id Tests
2028
+ // ============================================================================
2029
+ (0, vitest_1.describe)("agent_id on chat types", () => {
2030
+ (0, vitest_1.it)("CreateChatSessionRequest includes agent_id", () => {
2031
+ const req = {
2032
+ collections: [{ collection_name: "docs" }],
2033
+ llm_provider: "openai",
2034
+ agent_id: "my-agent",
2035
+ };
2036
+ (0, vitest_1.expect)(req.agent_id).toBe("my-agent");
2037
+ });
2038
+ (0, vitest_1.it)("CreateChatSessionRequest omits agent_id when undefined", () => {
2039
+ const req = {
2040
+ collections: [],
2041
+ llm_provider: "openai",
2042
+ };
2043
+ (0, vitest_1.expect)(req.agent_id).toBeUndefined();
2044
+ });
2045
+ (0, vitest_1.it)("ChatSession includes agent_id", () => {
2046
+ const session = {
2047
+ chat_id: "c1",
2048
+ created_at: "2026-01-01",
2049
+ updated_at: "2026-01-01",
2050
+ llm_provider: "openai",
2051
+ llm_model: "gpt-4",
2052
+ collections: [],
2053
+ agent_id: "bot-1",
2054
+ message_count: 0,
2055
+ };
2056
+ (0, vitest_1.expect)(session.agent_id).toBe("bot-1");
2057
+ });
2058
+ (0, vitest_1.it)("ChatSession allows missing agent_id", () => {
2059
+ const session = {
2060
+ chat_id: "c1",
2061
+ created_at: "2026-01-01",
2062
+ updated_at: "2026-01-01",
2063
+ llm_provider: "openai",
2064
+ llm_model: "gpt-4",
2065
+ collections: [],
2066
+ message_count: 0,
2067
+ };
2068
+ (0, vitest_1.expect)(session.agent_id).toBeUndefined();
2069
+ });
2070
+ });
2071
+ // ============================================================================
2072
+ // client_tools / confirm_tools / exclude_tools Tests
2073
+ // ============================================================================
2074
+ (0, vitest_1.describe)("ChatMessageRequest tool fields", () => {
2075
+ (0, vitest_1.it)("includes client_tools, confirm_tools, exclude_tools", () => {
2076
+ const req = {
2077
+ message: "hello",
2078
+ client_tools: [
2079
+ {
2080
+ name: "weather",
2081
+ description: "Get weather",
2082
+ parameters: { type: "object" },
2083
+ },
2084
+ ],
2085
+ confirm_tools: ["shell_exec"],
2086
+ exclude_tools: ["file_delete"],
2087
+ };
2088
+ (0, vitest_1.expect)(req.client_tools).toHaveLength(1);
2089
+ (0, vitest_1.expect)(req.client_tools[0].name).toBe("weather");
2090
+ (0, vitest_1.expect)(req.confirm_tools).toEqual(["shell_exec"]);
2091
+ (0, vitest_1.expect)(req.exclude_tools).toEqual(["file_delete"]);
2092
+ });
2093
+ (0, vitest_1.it)("tool fields are optional", () => {
2094
+ const req = { message: "hi" };
2095
+ (0, vitest_1.expect)(req.client_tools).toBeUndefined();
2096
+ (0, vitest_1.expect)(req.confirm_tools).toBeUndefined();
2097
+ (0, vitest_1.expect)(req.exclude_tools).toBeUndefined();
2098
+ });
2099
+ (0, vitest_1.it)("ClientToolDefinition has correct shape", () => {
2100
+ const tool = {
2101
+ name: "calc",
2102
+ description: "Calculator",
2103
+ parameters: { type: "object", properties: {} },
2104
+ };
2105
+ (0, vitest_1.expect)(tool.name).toBe("calc");
2106
+ (0, vitest_1.expect)(tool.description).toBe("Calculator");
2107
+ (0, vitest_1.expect)(tool.parameters.type).toBe("object");
2108
+ });
2109
+ });
2110
+ // ============================================================================
2111
+ // submitChatToolResult Tests
2112
+ // ============================================================================
2113
+ (0, vitest_1.describe)("submitChatToolResult", () => {
2114
+ (0, vitest_1.it)("sends tool result to correct endpoint", async () => {
2115
+ const client = createTestClient();
2116
+ mockTokenResponse();
2117
+ mockJsonResponse({});
2118
+ await client.submitChatToolResult("chat-123", "call-456", true, {
2119
+ temp: "72F",
2120
+ });
2121
+ (0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
2122
+ const call = mockFetch.mock.calls[1];
2123
+ (0, vitest_1.expect)(call[0]).toContain("/api/chat/chat-123/tool-result");
2124
+ const body = JSON.parse(call[1].body);
2125
+ (0, vitest_1.expect)(body.call_id).toBe("call-456");
2126
+ (0, vitest_1.expect)(body.success).toBe(true);
2127
+ (0, vitest_1.expect)(body.result.temp).toBe("72F");
2128
+ });
2129
+ (0, vitest_1.it)("sends error result", async () => {
2130
+ const client = createTestClient();
2131
+ mockTokenResponse();
2132
+ mockJsonResponse({});
2133
+ await client.submitChatToolResult("chat-123", "call-456", false, undefined, "tool crashed");
2134
+ const call = mockFetch.mock.calls[1];
2135
+ const body = JSON.parse(call[1].body);
2136
+ (0, vitest_1.expect)(body.success).toBe(false);
2137
+ (0, vitest_1.expect)(body.error).toBe("tool crashed");
2138
+ (0, vitest_1.expect)(body.result).toBeUndefined();
2139
+ });
2140
+ });
2141
+ // ============================================================================
2142
+ // subscribeSSE Tests
2143
+ // ============================================================================
2144
+ (0, vitest_1.describe)("subscribeSSE", () => {
2145
+ /** Create a mock ReadableStream from a string */
2146
+ function mockReadableStream(data) {
2147
+ const encoder = new TextEncoder();
2148
+ const bytes = encoder.encode(data);
2149
+ let sent = false;
2150
+ return {
2151
+ getReader: () => ({
2152
+ read: async () => {
2153
+ if (!sent) {
2154
+ sent = true;
2155
+ return { value: bytes, done: false };
2156
+ }
2157
+ return { value: undefined, done: true };
2158
+ },
2159
+ }),
2160
+ };
2161
+ }
2162
+ (0, vitest_1.it)("parses mutation events from SSE stream", async () => {
2163
+ const client = createTestClient();
2164
+ mockTokenResponse();
2165
+ const sseBody = "event: subscribed\ndata: {}\n\n" +
2166
+ 'event: mutation\ndata: {"collection":"orders","event":"insert","record_ids":["r1"],"timestamp":"t1"}\n\n' +
2167
+ 'event: mutation\ndata: {"collection":"orders","event":"update","record_ids":["r2"],"timestamp":"t2"}\n\n';
2168
+ mockFetch.mockResolvedValueOnce({
2169
+ ok: true,
2170
+ status: 200,
2171
+ body: mockReadableStream(sseBody),
2172
+ headers: new Headers({ "content-type": "text/event-stream" }),
2173
+ });
2174
+ const stream = client.subscribeSSE("orders");
2175
+ const events = [];
2176
+ await new Promise((resolve) => {
2177
+ stream.on("event", (e) => events.push(e));
2178
+ stream.on("close", resolve);
2179
+ setTimeout(resolve, 100);
2180
+ });
2181
+ (0, vitest_1.expect)(events).toHaveLength(2);
2182
+ (0, vitest_1.expect)(events[0].event).toBe("insert");
2183
+ (0, vitest_1.expect)(events[0].recordIds).toEqual(["r1"]);
2184
+ (0, vitest_1.expect)(events[1].event).toBe("update");
2185
+ (0, vitest_1.expect)(events[1].recordIds).toEqual(["r2"]);
2186
+ });
2187
+ (0, vitest_1.it)("passes filter params in URL", async () => {
2188
+ const client = createTestClient();
2189
+ mockTokenResponse();
2190
+ mockFetch.mockResolvedValueOnce({
2191
+ ok: true,
2192
+ status: 200,
2193
+ body: mockReadableStream(""),
2194
+ headers: new Headers({ "content-type": "text/event-stream" }),
2195
+ });
2196
+ client.subscribeSSE("orders", {
2197
+ filterField: "status",
2198
+ filterValue: "active",
2199
+ });
2200
+ // Wait for async fetch
2201
+ await new Promise((r) => setTimeout(r, 50));
2202
+ const call = mockFetch.mock.calls[1];
2203
+ (0, vitest_1.expect)(call[0]).toContain("/api/subscribe/orders");
2204
+ (0, vitest_1.expect)(call[0]).toContain("filter_field=status");
2205
+ (0, vitest_1.expect)(call[0]).toContain("filter_value=active");
2206
+ });
2207
+ (0, vitest_1.it)("emits error on HTTP failure", async () => {
2208
+ const client = createTestClient();
2209
+ mockTokenResponse();
2210
+ mockFetch.mockResolvedValueOnce({
2211
+ ok: false,
2212
+ status: 401,
2213
+ text: async () => "Unauthorized",
2214
+ headers: new Headers(),
2215
+ });
2216
+ const stream = client.subscribeSSE("orders");
2217
+ const errors = [];
2218
+ await new Promise((resolve) => {
2219
+ stream.on("error", (e) => errors.push(e));
2220
+ stream.on("close", resolve);
2221
+ setTimeout(resolve, 100);
2222
+ });
2223
+ (0, vitest_1.expect)(errors).toHaveLength(1);
2224
+ (0, vitest_1.expect)(errors[0]).toContain("401");
2225
+ });
2226
+ });