@ekodb/ekodb-client 0.13.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 +142 -4
- package/dist/client.js +486 -7
- package/dist/client.test.js +810 -0
- package/dist/websocket.test.js +89 -0
- package/package.json +1 -1
- package/src/client.test.ts +1016 -0
- package/src/client.ts +823 -7
- package/src/websocket.test.ts +137 -0
package/src/client.ts
CHANGED
|
@@ -371,6 +371,7 @@ export class EkoDBClient {
|
|
|
371
371
|
private baseURL: string;
|
|
372
372
|
private apiKey: string;
|
|
373
373
|
private token: string | null = null;
|
|
374
|
+
private tokenExpiry: number = 0;
|
|
374
375
|
private shouldRetry: boolean;
|
|
375
376
|
private maxRetries: number;
|
|
376
377
|
private format: SerializationFormat;
|
|
@@ -441,22 +442,74 @@ export class EkoDBClient {
|
|
|
441
442
|
|
|
442
443
|
const result = (await response.json()) as { token: string };
|
|
443
444
|
this.token = result.token;
|
|
445
|
+
|
|
446
|
+
// Extract and cache JWT expiry for proactive refresh
|
|
447
|
+
const expiry = this.extractJWTExpiry(result.token);
|
|
448
|
+
this.tokenExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600; // fallback: 1 hour
|
|
444
449
|
}
|
|
445
450
|
|
|
446
451
|
/**
|
|
447
|
-
* Get
|
|
448
|
-
*
|
|
449
|
-
|
|
450
|
-
|
|
452
|
+
* Get a valid authentication token.
|
|
453
|
+
*
|
|
454
|
+
* Returns a cached token if it has more than 60s of validity remaining.
|
|
455
|
+
* Otherwise fetches a new one via refreshToken(). This means callers
|
|
456
|
+
* never need to handle token refresh themselves — every getToken() call
|
|
457
|
+
* returns a token that's valid for at least 60 more seconds.
|
|
458
|
+
*/
|
|
459
|
+
async getToken(): Promise<string | null> {
|
|
460
|
+
if (this.token) {
|
|
461
|
+
const now = Math.floor(Date.now() / 1000);
|
|
462
|
+
if (now + 60 >= this.tokenExpiry) {
|
|
463
|
+
// Token is about to expire or already expired — refresh proactively
|
|
464
|
+
await this.refreshToken();
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// No token yet — fetch one
|
|
468
|
+
await this.refreshToken();
|
|
469
|
+
}
|
|
451
470
|
return this.token;
|
|
452
471
|
}
|
|
453
472
|
|
|
454
473
|
/**
|
|
455
|
-
* Clear the cached authentication token.
|
|
474
|
+
* Clear the cached authentication token and expiry.
|
|
456
475
|
* The next request will trigger a fresh token exchange.
|
|
457
476
|
*/
|
|
458
477
|
clearTokenCache(): void {
|
|
459
478
|
this.token = null;
|
|
479
|
+
this.tokenExpiry = 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Extract the `exp` claim from a JWT without verifying the signature.
|
|
484
|
+
* Returns the Unix timestamp (seconds) of expiry, or null if parsing fails.
|
|
485
|
+
*/
|
|
486
|
+
private extractJWTExpiry(token: string): number | null {
|
|
487
|
+
try {
|
|
488
|
+
const parts = token.split(".");
|
|
489
|
+
if (parts.length !== 3) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Convert base64url to standard base64
|
|
494
|
+
let payload = parts[1];
|
|
495
|
+
payload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
496
|
+
|
|
497
|
+
// Pad to multiple of 4
|
|
498
|
+
const pad = payload.length % 4;
|
|
499
|
+
if (pad) {
|
|
500
|
+
payload += "=".repeat(4 - pad);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const decoded = atob(payload);
|
|
504
|
+
const claims = JSON.parse(decoded);
|
|
505
|
+
|
|
506
|
+
if (typeof claims.exp === "number") {
|
|
507
|
+
return claims.exp;
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
460
513
|
}
|
|
461
514
|
|
|
462
515
|
/**
|
|
@@ -1542,6 +1595,115 @@ export class EkoDBClient {
|
|
|
1542
1595
|
);
|
|
1543
1596
|
}
|
|
1544
1597
|
|
|
1598
|
+
/**
|
|
1599
|
+
* Stateless raw LLM completion via SSE streaming.
|
|
1600
|
+
*
|
|
1601
|
+
* Same as rawCompletion() but uses Server-Sent Events to keep the
|
|
1602
|
+
* connection alive. Preferred for deployed instances where reverse proxies
|
|
1603
|
+
* may kill idle HTTP connections before the LLM responds.
|
|
1604
|
+
*/
|
|
1605
|
+
async rawCompletionStream(
|
|
1606
|
+
request: RawCompletionRequest,
|
|
1607
|
+
): Promise<RawCompletionResponse> {
|
|
1608
|
+
let token = await this.getToken();
|
|
1609
|
+
const url = `${this.baseURL}/api/chat/complete/stream`;
|
|
1610
|
+
|
|
1611
|
+
const response = await fetch(url, {
|
|
1612
|
+
method: "POST",
|
|
1613
|
+
headers: {
|
|
1614
|
+
"Content-Type": "application/json",
|
|
1615
|
+
Accept: "text/event-stream",
|
|
1616
|
+
Authorization: `Bearer ${token}`,
|
|
1617
|
+
},
|
|
1618
|
+
body: JSON.stringify(request),
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
if (!response.ok) {
|
|
1622
|
+
const body = await response.text();
|
|
1623
|
+
throw new Error(
|
|
1624
|
+
`SSE raw completion failed (${response.status}): ${body}`,
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const body = await response.text();
|
|
1629
|
+
let content = "";
|
|
1630
|
+
let lastError: string | null = null;
|
|
1631
|
+
|
|
1632
|
+
for (const line of body.split("\n")) {
|
|
1633
|
+
if (line.startsWith("data:")) {
|
|
1634
|
+
const dataStr = line.slice(5).trim();
|
|
1635
|
+
if (!dataStr) continue;
|
|
1636
|
+
try {
|
|
1637
|
+
const eventData = JSON.parse(dataStr);
|
|
1638
|
+
if (eventData.token) content += eventData.token;
|
|
1639
|
+
if (eventData.content) content = eventData.content;
|
|
1640
|
+
if (eventData.error) lastError = eventData.error;
|
|
1641
|
+
} catch {
|
|
1642
|
+
// skip malformed SSE data
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (lastError) throw new Error(lastError);
|
|
1648
|
+
return { content };
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Stateless raw LLM completion via SSE streaming with token-level progress.
|
|
1653
|
+
*
|
|
1654
|
+
* Same as rawCompletionStream() but invokes `onToken` with each token as it
|
|
1655
|
+
* arrives, allowing callers to show real-time progress.
|
|
1656
|
+
*/
|
|
1657
|
+
async rawCompletionStreamWithProgress(
|
|
1658
|
+
request: RawCompletionRequest,
|
|
1659
|
+
onToken: (token: string) => void,
|
|
1660
|
+
): Promise<RawCompletionResponse> {
|
|
1661
|
+
let token = await this.getToken();
|
|
1662
|
+
const url = `${this.baseURL}/api/chat/complete/stream`;
|
|
1663
|
+
|
|
1664
|
+
const response = await fetch(url, {
|
|
1665
|
+
method: "POST",
|
|
1666
|
+
headers: {
|
|
1667
|
+
"Content-Type": "application/json",
|
|
1668
|
+
Accept: "text/event-stream",
|
|
1669
|
+
Authorization: `Bearer ${token}`,
|
|
1670
|
+
},
|
|
1671
|
+
body: JSON.stringify(request),
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
if (!response.ok) {
|
|
1675
|
+
const body = await response.text();
|
|
1676
|
+
throw new Error(
|
|
1677
|
+
`SSE raw completion failed (${response.status}): ${body}`,
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const body = await response.text();
|
|
1682
|
+
let content = "";
|
|
1683
|
+
let lastError: string | null = null;
|
|
1684
|
+
|
|
1685
|
+
for (const line of body.split("\n")) {
|
|
1686
|
+
if (line.startsWith("data:")) {
|
|
1687
|
+
const dataStr = line.slice(5).trim();
|
|
1688
|
+
if (!dataStr) continue;
|
|
1689
|
+
try {
|
|
1690
|
+
const eventData = JSON.parse(dataStr);
|
|
1691
|
+
if (eventData.token) {
|
|
1692
|
+
content += eventData.token;
|
|
1693
|
+
onToken(eventData.token);
|
|
1694
|
+
}
|
|
1695
|
+
if (eventData.content) content = eventData.content;
|
|
1696
|
+
if (eventData.error) lastError = eventData.error;
|
|
1697
|
+
} catch {
|
|
1698
|
+
// skip malformed SSE data
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
if (lastError) throw new Error(lastError);
|
|
1704
|
+
return { content };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1545
1707
|
/**
|
|
1546
1708
|
* Send a message in an existing chat session
|
|
1547
1709
|
*/
|
|
@@ -1558,6 +1720,96 @@ export class EkoDBClient {
|
|
|
1558
1720
|
);
|
|
1559
1721
|
}
|
|
1560
1722
|
|
|
1723
|
+
/**
|
|
1724
|
+
* Send a message in an existing chat session via SSE streaming.
|
|
1725
|
+
*
|
|
1726
|
+
* Returns an EventStream that emits ChatStreamEvent objects as they arrive:
|
|
1727
|
+
* - `{ type: "chunk", content: "..." }` for each token
|
|
1728
|
+
* - `{ type: "end", messageId, executionTimeMs, tokenUsage?, contextWindow? }` when complete
|
|
1729
|
+
* - `{ type: "error", error: "..." }` on failure
|
|
1730
|
+
*
|
|
1731
|
+
* Preferred over chatMessage() for long-running responses where reverse
|
|
1732
|
+
* proxies may kill idle HTTP connections before the LLM responds.
|
|
1733
|
+
*/
|
|
1734
|
+
chatMessageStream(
|
|
1735
|
+
chatId: string,
|
|
1736
|
+
request: ChatMessageRequest,
|
|
1737
|
+
): EventStream<ChatStreamEvent> {
|
|
1738
|
+
const stream = new EventStream<ChatStreamEvent>();
|
|
1739
|
+
|
|
1740
|
+
(async () => {
|
|
1741
|
+
try {
|
|
1742
|
+
let token = this.getToken();
|
|
1743
|
+
if (!token) {
|
|
1744
|
+
await this.refreshToken();
|
|
1745
|
+
token = this.getToken();
|
|
1746
|
+
}
|
|
1747
|
+
const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
|
|
1748
|
+
|
|
1749
|
+
const response = await fetch(url, {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
headers: {
|
|
1752
|
+
"Content-Type": "application/json",
|
|
1753
|
+
Accept: "text/event-stream",
|
|
1754
|
+
Authorization: `Bearer ${token}`,
|
|
1755
|
+
},
|
|
1756
|
+
body: JSON.stringify(request),
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
if (!response.ok) {
|
|
1760
|
+
const body = await response.text();
|
|
1761
|
+
stream.emit("event", {
|
|
1762
|
+
type: "error",
|
|
1763
|
+
error: `SSE chat message stream failed (${response.status}): ${body}`,
|
|
1764
|
+
} as ChatStreamEvent);
|
|
1765
|
+
stream.close();
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const body = await response.text();
|
|
1770
|
+
for (const line of body.split("\n")) {
|
|
1771
|
+
if (!line.startsWith("data:")) continue;
|
|
1772
|
+
const dataStr = line.slice(5).trim();
|
|
1773
|
+
if (!dataStr) continue;
|
|
1774
|
+
try {
|
|
1775
|
+
const eventData = JSON.parse(dataStr);
|
|
1776
|
+
if (eventData.error) {
|
|
1777
|
+
stream.emit("event", {
|
|
1778
|
+
type: "error",
|
|
1779
|
+
error: eventData.error,
|
|
1780
|
+
} as ChatStreamEvent);
|
|
1781
|
+
} else if (eventData.content && eventData.message_id) {
|
|
1782
|
+
// Done event — has full content + message_id
|
|
1783
|
+
stream.emit("event", {
|
|
1784
|
+
type: "end",
|
|
1785
|
+
messageId: eventData.message_id,
|
|
1786
|
+
executionTimeMs: eventData.execution_time_ms ?? 0,
|
|
1787
|
+
tokenUsage: eventData.token_usage,
|
|
1788
|
+
contextWindow: eventData.context_window,
|
|
1789
|
+
} as ChatStreamEvent);
|
|
1790
|
+
} else if (eventData.token) {
|
|
1791
|
+
stream.emit("event", {
|
|
1792
|
+
type: "chunk",
|
|
1793
|
+
content: eventData.token,
|
|
1794
|
+
} as ChatStreamEvent);
|
|
1795
|
+
}
|
|
1796
|
+
} catch {
|
|
1797
|
+
// skip malformed SSE data
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
stream.close();
|
|
1801
|
+
} catch (err: any) {
|
|
1802
|
+
stream.emit("event", {
|
|
1803
|
+
type: "error",
|
|
1804
|
+
error: err.message ?? String(err),
|
|
1805
|
+
} as ChatStreamEvent);
|
|
1806
|
+
stream.close();
|
|
1807
|
+
}
|
|
1808
|
+
})();
|
|
1809
|
+
|
|
1810
|
+
return stream;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1561
1813
|
/**
|
|
1562
1814
|
* Get a chat session by ID
|
|
1563
1815
|
*/
|
|
@@ -1942,6 +2194,517 @@ export class EkoDBClient {
|
|
|
1942
2194
|
);
|
|
1943
2195
|
}
|
|
1944
2196
|
|
|
2197
|
+
// ========================================================================
|
|
2198
|
+
// GOAL API
|
|
2199
|
+
// ========================================================================
|
|
2200
|
+
|
|
2201
|
+
/** Create a new goal */
|
|
2202
|
+
async goalCreate(data: Record): Promise<Record> {
|
|
2203
|
+
return this.makeRequest<Record>("POST", "/api/chat/goals", data, 0, true);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/** List all goals */
|
|
2207
|
+
async goalList(): Promise<Record> {
|
|
2208
|
+
return this.makeRequest<Record>(
|
|
2209
|
+
"GET",
|
|
2210
|
+
"/api/chat/goals",
|
|
2211
|
+
undefined,
|
|
2212
|
+
0,
|
|
2213
|
+
true,
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/** Get a goal by ID */
|
|
2218
|
+
async goalGet(id: string): Promise<Record> {
|
|
2219
|
+
return this.makeRequest<Record>(
|
|
2220
|
+
"GET",
|
|
2221
|
+
`/api/chat/goals/${encodeURIComponent(id)}`,
|
|
2222
|
+
undefined,
|
|
2223
|
+
0,
|
|
2224
|
+
true,
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/** Update a goal by ID */
|
|
2229
|
+
async goalUpdate(id: string, data: Record): Promise<Record> {
|
|
2230
|
+
return this.makeRequest<Record>(
|
|
2231
|
+
"PUT",
|
|
2232
|
+
`/api/chat/goals/${encodeURIComponent(id)}`,
|
|
2233
|
+
data,
|
|
2234
|
+
0,
|
|
2235
|
+
true,
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/** Delete a goal by ID */
|
|
2240
|
+
async goalDelete(id: string): Promise<void> {
|
|
2241
|
+
await this.makeRequest<void>(
|
|
2242
|
+
"DELETE",
|
|
2243
|
+
`/api/chat/goals/${encodeURIComponent(id)}`,
|
|
2244
|
+
undefined,
|
|
2245
|
+
0,
|
|
2246
|
+
true,
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// ── Goal Templates ──
|
|
2251
|
+
|
|
2252
|
+
/** Create a new goal template */
|
|
2253
|
+
async goalTemplateCreate(data: Record): Promise<Record> {
|
|
2254
|
+
return this.makeRequest<Record>(
|
|
2255
|
+
"POST",
|
|
2256
|
+
"/api/chat/goal-templates",
|
|
2257
|
+
data,
|
|
2258
|
+
0,
|
|
2259
|
+
true,
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/** List all goal templates */
|
|
2264
|
+
async goalTemplateList(): Promise<Record> {
|
|
2265
|
+
return this.makeRequest<Record>(
|
|
2266
|
+
"GET",
|
|
2267
|
+
"/api/chat/goal-templates",
|
|
2268
|
+
undefined,
|
|
2269
|
+
0,
|
|
2270
|
+
true,
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/** Get a goal template by ID */
|
|
2275
|
+
async goalTemplateGet(id: string): Promise<Record> {
|
|
2276
|
+
return this.makeRequest<Record>(
|
|
2277
|
+
"GET",
|
|
2278
|
+
`/api/chat/goal-templates/${encodeURIComponent(id)}`,
|
|
2279
|
+
undefined,
|
|
2280
|
+
0,
|
|
2281
|
+
true,
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/** Update a goal template by ID */
|
|
2286
|
+
async goalTemplateUpdate(id: string, data: Record): Promise<Record> {
|
|
2287
|
+
return this.makeRequest<Record>(
|
|
2288
|
+
"PUT",
|
|
2289
|
+
`/api/chat/goal-templates/${encodeURIComponent(id)}`,
|
|
2290
|
+
data,
|
|
2291
|
+
0,
|
|
2292
|
+
true,
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/** Delete a goal template by ID */
|
|
2297
|
+
async goalTemplateDelete(id: string): Promise<void> {
|
|
2298
|
+
await this.makeRequest<void>(
|
|
2299
|
+
"DELETE",
|
|
2300
|
+
`/api/chat/goal-templates/${encodeURIComponent(id)}`,
|
|
2301
|
+
undefined,
|
|
2302
|
+
0,
|
|
2303
|
+
true,
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/** Search goals */
|
|
2308
|
+
async goalSearch(query: string): Promise<Record> {
|
|
2309
|
+
const params = new URLSearchParams({ q: query });
|
|
2310
|
+
return this.makeRequest<Record>(
|
|
2311
|
+
"GET",
|
|
2312
|
+
`/api/chat/goals/search?${params}`,
|
|
2313
|
+
undefined,
|
|
2314
|
+
0,
|
|
2315
|
+
true,
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/** Mark a goal as complete (status -> pending_review) */
|
|
2320
|
+
async goalComplete(id: string, data: Record): Promise<Record> {
|
|
2321
|
+
return this.makeRequest<Record>(
|
|
2322
|
+
"POST",
|
|
2323
|
+
`/api/chat/goals/${encodeURIComponent(id)}/complete`,
|
|
2324
|
+
data,
|
|
2325
|
+
0,
|
|
2326
|
+
true,
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/** Approve a goal (status -> in_progress) */
|
|
2331
|
+
async goalApprove(id: string): Promise<Record> {
|
|
2332
|
+
return this.makeRequest<Record>(
|
|
2333
|
+
"POST",
|
|
2334
|
+
`/api/chat/goals/${encodeURIComponent(id)}/approve`,
|
|
2335
|
+
undefined,
|
|
2336
|
+
0,
|
|
2337
|
+
true,
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/** Reject a goal (status -> failed) */
|
|
2342
|
+
async goalReject(id: string, data: Record): Promise<Record> {
|
|
2343
|
+
return this.makeRequest<Record>(
|
|
2344
|
+
"POST",
|
|
2345
|
+
`/api/chat/goals/${encodeURIComponent(id)}/reject`,
|
|
2346
|
+
data,
|
|
2347
|
+
0,
|
|
2348
|
+
true,
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/** Start a goal step (status -> in_progress) */
|
|
2353
|
+
async goalStepStart(id: string, stepIndex: number): Promise<Record> {
|
|
2354
|
+
return this.makeRequest<Record>(
|
|
2355
|
+
"POST",
|
|
2356
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`,
|
|
2357
|
+
undefined,
|
|
2358
|
+
0,
|
|
2359
|
+
true,
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/** Complete a goal step with result */
|
|
2364
|
+
async goalStepComplete(
|
|
2365
|
+
id: string,
|
|
2366
|
+
stepIndex: number,
|
|
2367
|
+
data: Record,
|
|
2368
|
+
): Promise<Record> {
|
|
2369
|
+
return this.makeRequest<Record>(
|
|
2370
|
+
"POST",
|
|
2371
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`,
|
|
2372
|
+
data,
|
|
2373
|
+
0,
|
|
2374
|
+
true,
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/** Fail a goal step with error */
|
|
2379
|
+
async goalStepFail(
|
|
2380
|
+
id: string,
|
|
2381
|
+
stepIndex: number,
|
|
2382
|
+
data: Record,
|
|
2383
|
+
): Promise<Record> {
|
|
2384
|
+
return this.makeRequest<Record>(
|
|
2385
|
+
"POST",
|
|
2386
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`,
|
|
2387
|
+
data,
|
|
2388
|
+
0,
|
|
2389
|
+
true,
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// ========================================================================
|
|
2394
|
+
// TASK API
|
|
2395
|
+
// ========================================================================
|
|
2396
|
+
|
|
2397
|
+
/** Create a new scheduled task */
|
|
2398
|
+
async taskCreate(data: Record): Promise<Record> {
|
|
2399
|
+
return this.makeRequest<Record>("POST", "/api/chat/tasks", data, 0, true);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
/** List all scheduled tasks */
|
|
2403
|
+
async taskList(): Promise<Record> {
|
|
2404
|
+
return this.makeRequest<Record>(
|
|
2405
|
+
"GET",
|
|
2406
|
+
"/api/chat/tasks",
|
|
2407
|
+
undefined,
|
|
2408
|
+
0,
|
|
2409
|
+
true,
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/** Get a task by ID */
|
|
2414
|
+
async taskGet(id: string): Promise<Record> {
|
|
2415
|
+
return this.makeRequest<Record>(
|
|
2416
|
+
"GET",
|
|
2417
|
+
`/api/chat/tasks/${encodeURIComponent(id)}`,
|
|
2418
|
+
undefined,
|
|
2419
|
+
0,
|
|
2420
|
+
true,
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
/** Update a task by ID */
|
|
2425
|
+
async taskUpdate(id: string, data: Record): Promise<Record> {
|
|
2426
|
+
return this.makeRequest<Record>(
|
|
2427
|
+
"PUT",
|
|
2428
|
+
`/api/chat/tasks/${encodeURIComponent(id)}`,
|
|
2429
|
+
data,
|
|
2430
|
+
0,
|
|
2431
|
+
true,
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** Delete a task by ID */
|
|
2436
|
+
async taskDelete(id: string): Promise<void> {
|
|
2437
|
+
await this.makeRequest<void>(
|
|
2438
|
+
"DELETE",
|
|
2439
|
+
`/api/chat/tasks/${encodeURIComponent(id)}`,
|
|
2440
|
+
undefined,
|
|
2441
|
+
0,
|
|
2442
|
+
true,
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
/** Get tasks that are due at the given time */
|
|
2447
|
+
async taskDue(now: string): Promise<Record> {
|
|
2448
|
+
const params = new URLSearchParams({ now });
|
|
2449
|
+
return this.makeRequest<Record>(
|
|
2450
|
+
"GET",
|
|
2451
|
+
`/api/chat/tasks/due?${params}`,
|
|
2452
|
+
undefined,
|
|
2453
|
+
0,
|
|
2454
|
+
true,
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/** Start a task (status -> running) */
|
|
2459
|
+
async taskStart(id: string): Promise<Record> {
|
|
2460
|
+
return this.makeRequest<Record>(
|
|
2461
|
+
"POST",
|
|
2462
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/start`,
|
|
2463
|
+
undefined,
|
|
2464
|
+
0,
|
|
2465
|
+
true,
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/** Mark a task as succeeded */
|
|
2470
|
+
async taskSucceed(id: string, data: Record): Promise<Record> {
|
|
2471
|
+
return this.makeRequest<Record>(
|
|
2472
|
+
"POST",
|
|
2473
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/succeed`,
|
|
2474
|
+
data,
|
|
2475
|
+
0,
|
|
2476
|
+
true,
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
/** Mark a task as failed */
|
|
2481
|
+
async taskFail(id: string, data: Record): Promise<Record> {
|
|
2482
|
+
return this.makeRequest<Record>(
|
|
2483
|
+
"POST",
|
|
2484
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/fail`,
|
|
2485
|
+
data,
|
|
2486
|
+
0,
|
|
2487
|
+
true,
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/** Pause a task */
|
|
2492
|
+
async taskPause(id: string): Promise<Record> {
|
|
2493
|
+
return this.makeRequest<Record>(
|
|
2494
|
+
"POST",
|
|
2495
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/pause`,
|
|
2496
|
+
undefined,
|
|
2497
|
+
0,
|
|
2498
|
+
true,
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/** Resume a paused task */
|
|
2503
|
+
async taskResume(id: string, data: Record): Promise<Record> {
|
|
2504
|
+
return this.makeRequest<Record>(
|
|
2505
|
+
"POST",
|
|
2506
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/resume`,
|
|
2507
|
+
data,
|
|
2508
|
+
0,
|
|
2509
|
+
true,
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// ========================================================================
|
|
2514
|
+
// AGENT API
|
|
2515
|
+
// ========================================================================
|
|
2516
|
+
|
|
2517
|
+
/** Create a new agent */
|
|
2518
|
+
async agentCreate(data: Record): Promise<Record> {
|
|
2519
|
+
return this.makeRequest<Record>("POST", "/api/chat/agents", data, 0, true);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/** List all agents */
|
|
2523
|
+
async agentList(): Promise<Record> {
|
|
2524
|
+
return this.makeRequest<Record>(
|
|
2525
|
+
"GET",
|
|
2526
|
+
"/api/chat/agents",
|
|
2527
|
+
undefined,
|
|
2528
|
+
0,
|
|
2529
|
+
true,
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** Get an agent by ID */
|
|
2534
|
+
async agentGet(id: string): Promise<Record> {
|
|
2535
|
+
return this.makeRequest<Record>(
|
|
2536
|
+
"GET",
|
|
2537
|
+
`/api/chat/agents/${encodeURIComponent(id)}`,
|
|
2538
|
+
undefined,
|
|
2539
|
+
0,
|
|
2540
|
+
true,
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/** Get an agent by name */
|
|
2545
|
+
async agentGetByName(name: string): Promise<Record> {
|
|
2546
|
+
return this.makeRequest<Record>(
|
|
2547
|
+
"GET",
|
|
2548
|
+
`/api/chat/agents/by-name/${encodeURIComponent(name)}`,
|
|
2549
|
+
undefined,
|
|
2550
|
+
0,
|
|
2551
|
+
true,
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/** Update an agent by ID */
|
|
2556
|
+
async agentUpdate(id: string, data: Record): Promise<Record> {
|
|
2557
|
+
return this.makeRequest<Record>(
|
|
2558
|
+
"PUT",
|
|
2559
|
+
`/api/chat/agents/${encodeURIComponent(id)}`,
|
|
2560
|
+
data,
|
|
2561
|
+
0,
|
|
2562
|
+
true,
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/** Delete an agent by ID */
|
|
2567
|
+
async agentDelete(id: string): Promise<void> {
|
|
2568
|
+
await this.makeRequest<void>(
|
|
2569
|
+
"DELETE",
|
|
2570
|
+
`/api/chat/agents/${encodeURIComponent(id)}`,
|
|
2571
|
+
undefined,
|
|
2572
|
+
0,
|
|
2573
|
+
true,
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/** Get agents by deployment ID */
|
|
2578
|
+
async agentsByDeployment(deploymentId: string): Promise<Record> {
|
|
2579
|
+
return this.makeRequest<Record>(
|
|
2580
|
+
"GET",
|
|
2581
|
+
`/api/chat/agents/by-deployment/${encodeURIComponent(deploymentId)}`,
|
|
2582
|
+
undefined,
|
|
2583
|
+
0,
|
|
2584
|
+
true,
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// ========================================================================
|
|
2589
|
+
// KV DOCUMENT LINKING
|
|
2590
|
+
// ========================================================================
|
|
2591
|
+
|
|
2592
|
+
/** Get documents linked to a KV key */
|
|
2593
|
+
async kvGetLinks(key: string): Promise<Record> {
|
|
2594
|
+
return this.makeRequest<Record>(
|
|
2595
|
+
"GET",
|
|
2596
|
+
`/api/kv/links/${encodeURIComponent(key)}`,
|
|
2597
|
+
undefined,
|
|
2598
|
+
0,
|
|
2599
|
+
true,
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/** Link a document to a KV key */
|
|
2604
|
+
async kvLink(
|
|
2605
|
+
key: string,
|
|
2606
|
+
collection: string,
|
|
2607
|
+
documentId: string,
|
|
2608
|
+
): Promise<Record> {
|
|
2609
|
+
return this.makeRequest<Record>(
|
|
2610
|
+
"POST",
|
|
2611
|
+
`/api/kv/link`,
|
|
2612
|
+
{ key, collection, document_id: documentId },
|
|
2613
|
+
0,
|
|
2614
|
+
true,
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
/** Unlink a document from a KV key */
|
|
2619
|
+
async kvUnlink(
|
|
2620
|
+
key: string,
|
|
2621
|
+
collection: string,
|
|
2622
|
+
documentId: string,
|
|
2623
|
+
): Promise<Record> {
|
|
2624
|
+
return this.makeRequest<Record>(
|
|
2625
|
+
"POST",
|
|
2626
|
+
`/api/kv/unlink`,
|
|
2627
|
+
{ key, collection, document_id: documentId },
|
|
2628
|
+
0,
|
|
2629
|
+
true,
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// ========================================================================
|
|
2634
|
+
// SCHEDULE MANAGEMENT
|
|
2635
|
+
// ========================================================================
|
|
2636
|
+
|
|
2637
|
+
/** Create a new schedule */
|
|
2638
|
+
async createSchedule(data: Record): Promise<Record> {
|
|
2639
|
+
return this.makeRequest<Record>("POST", `/api/schedules`, data, 0, true);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/** List all schedules */
|
|
2643
|
+
async listSchedules(): Promise<Record> {
|
|
2644
|
+
return this.makeRequest<Record>(
|
|
2645
|
+
"GET",
|
|
2646
|
+
`/api/schedules`,
|
|
2647
|
+
undefined,
|
|
2648
|
+
0,
|
|
2649
|
+
true,
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
/** Get a schedule by ID */
|
|
2654
|
+
async getSchedule(id: string): Promise<Record> {
|
|
2655
|
+
return this.makeRequest<Record>(
|
|
2656
|
+
"GET",
|
|
2657
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
2658
|
+
undefined,
|
|
2659
|
+
0,
|
|
2660
|
+
true,
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/** Update a schedule */
|
|
2665
|
+
async updateSchedule(id: string, data: Record): Promise<Record> {
|
|
2666
|
+
return this.makeRequest<Record>(
|
|
2667
|
+
"PUT",
|
|
2668
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
2669
|
+
data,
|
|
2670
|
+
0,
|
|
2671
|
+
true,
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
/** Delete a schedule */
|
|
2676
|
+
async deleteSchedule(id: string): Promise<void> {
|
|
2677
|
+
await this.makeRequest<void>(
|
|
2678
|
+
"DELETE",
|
|
2679
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
2680
|
+
undefined,
|
|
2681
|
+
0,
|
|
2682
|
+
true,
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/** Pause a schedule */
|
|
2687
|
+
async pauseSchedule(id: string): Promise<Record> {
|
|
2688
|
+
return this.makeRequest<Record>(
|
|
2689
|
+
"POST",
|
|
2690
|
+
`/api/schedules/${encodeURIComponent(id)}/pause`,
|
|
2691
|
+
undefined,
|
|
2692
|
+
0,
|
|
2693
|
+
true,
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/** Resume a schedule */
|
|
2698
|
+
async resumeSchedule(id: string): Promise<Record> {
|
|
2699
|
+
return this.makeRequest<Record>(
|
|
2700
|
+
"POST",
|
|
2701
|
+
`/api/schedules/${encodeURIComponent(id)}/resume`,
|
|
2702
|
+
undefined,
|
|
2703
|
+
0,
|
|
2704
|
+
true,
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
1945
2708
|
// ========================================================================
|
|
1946
2709
|
// COLLECTION UTILITIES
|
|
1947
2710
|
// ========================================================================
|
|
@@ -2143,6 +2906,8 @@ export type ChatStreamEvent =
|
|
|
2143
2906
|
tokenUsage?: any;
|
|
2144
2907
|
toolCallHistory?: any;
|
|
2145
2908
|
executionTimeMs: number;
|
|
2909
|
+
/** Model's context window size in tokens. */
|
|
2910
|
+
contextWindow?: number;
|
|
2146
2911
|
}
|
|
2147
2912
|
| {
|
|
2148
2913
|
type: "toolCall";
|
|
@@ -2309,7 +3074,13 @@ export class WebSocketClient {
|
|
|
2309
3074
|
switch (msg.type) {
|
|
2310
3075
|
case "Success":
|
|
2311
3076
|
case "Error": {
|
|
2312
|
-
|
|
3077
|
+
// Try messageId from top-level, then from payload
|
|
3078
|
+
const messageId =
|
|
3079
|
+
msg.messageId ||
|
|
3080
|
+
msg.message_id ||
|
|
3081
|
+
msg.payload?.message_id ||
|
|
3082
|
+
msg.payload?.messageId;
|
|
3083
|
+
let matched = false;
|
|
2313
3084
|
if (messageId && this.pendingRequests.has(messageId)) {
|
|
2314
3085
|
const pending = this.pendingRequests.get(messageId)!;
|
|
2315
3086
|
this.pendingRequests.delete(messageId);
|
|
@@ -2318,7 +3089,9 @@ export class WebSocketClient {
|
|
|
2318
3089
|
} else {
|
|
2319
3090
|
pending.resolve(msg.payload);
|
|
2320
3091
|
}
|
|
2321
|
-
|
|
3092
|
+
matched = true;
|
|
3093
|
+
}
|
|
3094
|
+
if (!matched && this.registerToolsAck) {
|
|
2322
3095
|
const ack = this.registerToolsAck;
|
|
2323
3096
|
this.registerToolsAck = null;
|
|
2324
3097
|
if (msg.type === "Error") {
|
|
@@ -2326,6 +3099,20 @@ export class WebSocketClient {
|
|
|
2326
3099
|
} else {
|
|
2327
3100
|
ack.resolve(msg.payload);
|
|
2328
3101
|
}
|
|
3102
|
+
matched = true;
|
|
3103
|
+
}
|
|
3104
|
+
// Server doesn't echo messageId — if there's exactly one pending
|
|
3105
|
+
// request, deliver the response to it (sequential request/response).
|
|
3106
|
+
if (!matched && this.pendingRequests.size === 1) {
|
|
3107
|
+
const entry = this.pendingRequests.entries().next().value!;
|
|
3108
|
+
const key = entry[0];
|
|
3109
|
+
const pending = entry[1];
|
|
3110
|
+
this.pendingRequests.delete(key);
|
|
3111
|
+
if (msg.type === "Error") {
|
|
3112
|
+
pending.reject(new Error(msg.message || "Unknown error"));
|
|
3113
|
+
} else {
|
|
3114
|
+
pending.resolve(msg.payload);
|
|
3115
|
+
}
|
|
2329
3116
|
}
|
|
2330
3117
|
break;
|
|
2331
3118
|
}
|
|
@@ -2371,6 +3158,8 @@ export class WebSocketClient {
|
|
|
2371
3158
|
msg.payload.tool_call_history || msg.payload.toolCallHistory,
|
|
2372
3159
|
executionTimeMs:
|
|
2373
3160
|
msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
|
|
3161
|
+
contextWindow:
|
|
3162
|
+
msg.payload.context_window || msg.payload.contextWindow,
|
|
2374
3163
|
} as ChatStreamEvent);
|
|
2375
3164
|
this.chatStreams.delete(chatId);
|
|
2376
3165
|
stream.close();
|
|
@@ -2566,6 +3355,33 @@ export class WebSocketClient {
|
|
|
2566
3355
|
this.ws.send(JSON.stringify(request));
|
|
2567
3356
|
}
|
|
2568
3357
|
|
|
3358
|
+
/**
|
|
3359
|
+
* Stateless raw LLM completion via WebSocket.
|
|
3360
|
+
*
|
|
3361
|
+
* Sends a RawComplete message and waits for the Success response.
|
|
3362
|
+
* Preferred over HTTP for deployed instances: the persistent WSS
|
|
3363
|
+
* connection is already authenticated and won't be killed by reverse
|
|
3364
|
+
* proxy timeouts.
|
|
3365
|
+
*/
|
|
3366
|
+
async rawCompletion(
|
|
3367
|
+
request: RawCompletionRequest,
|
|
3368
|
+
): Promise<RawCompletionResponse> {
|
|
3369
|
+
await this.ensureConnected();
|
|
3370
|
+
const messageId = this.genMessageId();
|
|
3371
|
+
const payload = await this.sendRequest({
|
|
3372
|
+
type: "RawComplete",
|
|
3373
|
+
messageId,
|
|
3374
|
+
payload: {
|
|
3375
|
+
system_prompt: request.system_prompt,
|
|
3376
|
+
message: request.message,
|
|
3377
|
+
...(request.provider && { provider: request.provider }),
|
|
3378
|
+
...(request.model && { model: request.model }),
|
|
3379
|
+
...(request.max_tokens != null && { max_tokens: request.max_tokens }),
|
|
3380
|
+
},
|
|
3381
|
+
});
|
|
3382
|
+
return { content: payload?.data?.content || "" };
|
|
3383
|
+
}
|
|
3384
|
+
|
|
2569
3385
|
/**
|
|
2570
3386
|
* Close the WebSocket connection.
|
|
2571
3387
|
*/
|