@ekodb/ekodb-client 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -203,6 +203,7 @@ export interface ChatMessageRequest {
203
203
  force_summarize?: boolean;
204
204
  max_iterations?: number;
205
205
  tool_config?: ToolConfig;
206
+ llm_model?: string;
206
207
  }
207
208
 
208
209
  export interface TokenUsage {
@@ -371,6 +372,7 @@ export class EkoDBClient {
371
372
  private baseURL: string;
372
373
  private apiKey: string;
373
374
  private token: string | null = null;
375
+ private tokenExpiry: number = 0;
374
376
  private shouldRetry: boolean;
375
377
  private maxRetries: number;
376
378
  private format: SerializationFormat;
@@ -441,22 +443,74 @@ export class EkoDBClient {
441
443
 
442
444
  const result = (await response.json()) as { token: string };
443
445
  this.token = result.token;
446
+
447
+ // Extract and cache JWT expiry for proactive refresh
448
+ const expiry = this.extractJWTExpiry(result.token);
449
+ this.tokenExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600; // fallback: 1 hour
444
450
  }
445
451
 
446
452
  /**
447
- * Get the current authentication token.
448
- * Returns null if not yet authenticated. Call refreshToken() first.
449
- */
450
- getToken(): string | null {
453
+ * Get a valid authentication token.
454
+ *
455
+ * Returns a cached token if it has more than 60s of validity remaining.
456
+ * Otherwise fetches a new one via refreshToken(). This means callers
457
+ * never need to handle token refresh themselves — every getToken() call
458
+ * returns a token that's valid for at least 60 more seconds.
459
+ */
460
+ async getToken(): Promise<string | null> {
461
+ if (this.token) {
462
+ const now = Math.floor(Date.now() / 1000);
463
+ if (now + 60 >= this.tokenExpiry) {
464
+ // Token is about to expire or already expired — refresh proactively
465
+ await this.refreshToken();
466
+ }
467
+ } else {
468
+ // No token yet — fetch one
469
+ await this.refreshToken();
470
+ }
451
471
  return this.token;
452
472
  }
453
473
 
454
474
  /**
455
- * Clear the cached authentication token.
475
+ * Clear the cached authentication token and expiry.
456
476
  * The next request will trigger a fresh token exchange.
457
477
  */
458
478
  clearTokenCache(): void {
459
479
  this.token = null;
480
+ this.tokenExpiry = 0;
481
+ }
482
+
483
+ /**
484
+ * Extract the `exp` claim from a JWT without verifying the signature.
485
+ * Returns the Unix timestamp (seconds) of expiry, or null if parsing fails.
486
+ */
487
+ private extractJWTExpiry(token: string): number | null {
488
+ try {
489
+ const parts = token.split(".");
490
+ if (parts.length !== 3) {
491
+ return null;
492
+ }
493
+
494
+ // Convert base64url to standard base64
495
+ let payload = parts[1];
496
+ payload = payload.replace(/-/g, "+").replace(/_/g, "/");
497
+
498
+ // Pad to multiple of 4
499
+ const pad = payload.length % 4;
500
+ if (pad) {
501
+ payload += "=".repeat(4 - pad);
502
+ }
503
+
504
+ const decoded = atob(payload);
505
+ const claims = JSON.parse(decoded);
506
+
507
+ if (typeof claims.exp === "number") {
508
+ return claims.exp;
509
+ }
510
+ return null;
511
+ } catch {
512
+ return null;
513
+ }
460
514
  }
461
515
 
462
516
  /**
@@ -1499,6 +1553,54 @@ export class EkoDBClient {
1499
1553
 
1500
1554
  // ========== Chat Methods ==========
1501
1555
 
1556
+ /**
1557
+ * Execute a tool via ekoDB's server-side tool pipeline.
1558
+ *
1559
+ * Calls POST /api/chat/tools/execute which goes through the same
1560
+ * execute_tool function as the LLM tool-calling loop — with all
1561
+ * collection filtering, permission enforcement, and internal collection
1562
+ * blocking. No LLM round-trip.
1563
+ *
1564
+ * @returns The tool result if executed, or null if the server doesn't
1565
+ * support the endpoint (older ekoDB versions).
1566
+ */
1567
+ async executeTool(
1568
+ toolName: string,
1569
+ params: { [key: string]: any },
1570
+ chatId?: string,
1571
+ ): Promise<any | null> {
1572
+ const body: { [key: string]: any } = { tool: toolName, params };
1573
+ if (chatId) {
1574
+ body.chat_id = chatId;
1575
+ }
1576
+
1577
+ try {
1578
+ const result = await this.makeRequest<{ [key: string]: any }>(
1579
+ "POST",
1580
+ "/api/chat/tools/execute",
1581
+ body,
1582
+ 0,
1583
+ true, // Force JSON for chat operations
1584
+ );
1585
+
1586
+ if (result.success) {
1587
+ return result.result;
1588
+ } else {
1589
+ throw new Error(result.error || "tool execution failed");
1590
+ }
1591
+ } catch (err: any) {
1592
+ // Server doesn't have the endpoint (404) or route mismatch (405)
1593
+ // Parse status from makeRequest error format: "Request failed with status NNN: ..."
1594
+ const message = String(err?.message ?? "");
1595
+ const match = message.match(/Request failed with status (\d+):/);
1596
+ const status = match ? parseInt(match[1], 10) : undefined;
1597
+ if (status === 404 || status === 405) {
1598
+ return null;
1599
+ }
1600
+ throw err;
1601
+ }
1602
+ }
1603
+
1502
1604
  /**
1503
1605
  * Create a new chat session
1504
1606
  */
@@ -1542,6 +1644,115 @@ export class EkoDBClient {
1542
1644
  );
1543
1645
  }
1544
1646
 
1647
+ /**
1648
+ * Stateless raw LLM completion via SSE streaming.
1649
+ *
1650
+ * Same as rawCompletion() but uses Server-Sent Events to keep the
1651
+ * connection alive. Preferred for deployed instances where reverse proxies
1652
+ * may kill idle HTTP connections before the LLM responds.
1653
+ */
1654
+ async rawCompletionStream(
1655
+ request: RawCompletionRequest,
1656
+ ): Promise<RawCompletionResponse> {
1657
+ let token = await this.getToken();
1658
+ const url = `${this.baseURL}/api/chat/complete/stream`;
1659
+
1660
+ const response = await fetch(url, {
1661
+ method: "POST",
1662
+ headers: {
1663
+ "Content-Type": "application/json",
1664
+ Accept: "text/event-stream",
1665
+ Authorization: `Bearer ${token}`,
1666
+ },
1667
+ body: JSON.stringify(request),
1668
+ });
1669
+
1670
+ if (!response.ok) {
1671
+ const body = await response.text();
1672
+ throw new Error(
1673
+ `SSE raw completion failed (${response.status}): ${body}`,
1674
+ );
1675
+ }
1676
+
1677
+ const body = await response.text();
1678
+ let content = "";
1679
+ let lastError: string | null = null;
1680
+
1681
+ for (const line of body.split("\n")) {
1682
+ if (line.startsWith("data:")) {
1683
+ const dataStr = line.slice(5).trim();
1684
+ if (!dataStr) continue;
1685
+ try {
1686
+ const eventData = JSON.parse(dataStr);
1687
+ if (eventData.token) content += eventData.token;
1688
+ if (eventData.content) content = eventData.content;
1689
+ if (eventData.error) lastError = eventData.error;
1690
+ } catch {
1691
+ // skip malformed SSE data
1692
+ }
1693
+ }
1694
+ }
1695
+
1696
+ if (lastError) throw new Error(lastError);
1697
+ return { content };
1698
+ }
1699
+
1700
+ /**
1701
+ * Stateless raw LLM completion via SSE streaming with token-level progress.
1702
+ *
1703
+ * Same as rawCompletionStream() but invokes `onToken` with each token as it
1704
+ * arrives, allowing callers to show real-time progress.
1705
+ */
1706
+ async rawCompletionStreamWithProgress(
1707
+ request: RawCompletionRequest,
1708
+ onToken: (token: string) => void,
1709
+ ): Promise<RawCompletionResponse> {
1710
+ let token = await this.getToken();
1711
+ const url = `${this.baseURL}/api/chat/complete/stream`;
1712
+
1713
+ const response = await fetch(url, {
1714
+ method: "POST",
1715
+ headers: {
1716
+ "Content-Type": "application/json",
1717
+ Accept: "text/event-stream",
1718
+ Authorization: `Bearer ${token}`,
1719
+ },
1720
+ body: JSON.stringify(request),
1721
+ });
1722
+
1723
+ if (!response.ok) {
1724
+ const body = await response.text();
1725
+ throw new Error(
1726
+ `SSE raw completion failed (${response.status}): ${body}`,
1727
+ );
1728
+ }
1729
+
1730
+ const body = await response.text();
1731
+ let content = "";
1732
+ let lastError: string | null = null;
1733
+
1734
+ for (const line of body.split("\n")) {
1735
+ if (line.startsWith("data:")) {
1736
+ const dataStr = line.slice(5).trim();
1737
+ if (!dataStr) continue;
1738
+ try {
1739
+ const eventData = JSON.parse(dataStr);
1740
+ if (eventData.token) {
1741
+ content += eventData.token;
1742
+ onToken(eventData.token);
1743
+ }
1744
+ if (eventData.content) content = eventData.content;
1745
+ if (eventData.error) lastError = eventData.error;
1746
+ } catch {
1747
+ // skip malformed SSE data
1748
+ }
1749
+ }
1750
+ }
1751
+
1752
+ if (lastError) throw new Error(lastError);
1753
+ return { content };
1754
+ }
1755
+
1545
1756
  /**
1546
1757
  * Send a message in an existing chat session
1547
1758
  */
@@ -1558,6 +1769,96 @@ export class EkoDBClient {
1558
1769
  );
1559
1770
  }
1560
1771
 
1772
+ /**
1773
+ * Send a message in an existing chat session via SSE streaming.
1774
+ *
1775
+ * Returns an EventStream that emits ChatStreamEvent objects as they arrive:
1776
+ * - `{ type: "chunk", content: "..." }` for each token
1777
+ * - `{ type: "end", messageId, executionTimeMs, tokenUsage?, contextWindow? }` when complete
1778
+ * - `{ type: "error", error: "..." }` on failure
1779
+ *
1780
+ * Preferred over chatMessage() for long-running responses where reverse
1781
+ * proxies may kill idle HTTP connections before the LLM responds.
1782
+ */
1783
+ chatMessageStream(
1784
+ chatId: string,
1785
+ request: ChatMessageRequest,
1786
+ ): EventStream<ChatStreamEvent> {
1787
+ const stream = new EventStream<ChatStreamEvent>();
1788
+
1789
+ (async () => {
1790
+ try {
1791
+ let token = this.getToken();
1792
+ if (!token) {
1793
+ await this.refreshToken();
1794
+ token = this.getToken();
1795
+ }
1796
+ const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
1797
+
1798
+ const response = await fetch(url, {
1799
+ method: "POST",
1800
+ headers: {
1801
+ "Content-Type": "application/json",
1802
+ Accept: "text/event-stream",
1803
+ Authorization: `Bearer ${token}`,
1804
+ },
1805
+ body: JSON.stringify(request),
1806
+ });
1807
+
1808
+ if (!response.ok) {
1809
+ const body = await response.text();
1810
+ stream.emit("event", {
1811
+ type: "error",
1812
+ error: `SSE chat message stream failed (${response.status}): ${body}`,
1813
+ } as ChatStreamEvent);
1814
+ stream.close();
1815
+ return;
1816
+ }
1817
+
1818
+ const body = await response.text();
1819
+ for (const line of body.split("\n")) {
1820
+ if (!line.startsWith("data:")) continue;
1821
+ const dataStr = line.slice(5).trim();
1822
+ if (!dataStr) continue;
1823
+ try {
1824
+ const eventData = JSON.parse(dataStr);
1825
+ if (eventData.error) {
1826
+ stream.emit("event", {
1827
+ type: "error",
1828
+ error: eventData.error,
1829
+ } as ChatStreamEvent);
1830
+ } else if (eventData.content && eventData.message_id) {
1831
+ // Done event — has full content + message_id
1832
+ stream.emit("event", {
1833
+ type: "end",
1834
+ messageId: eventData.message_id,
1835
+ executionTimeMs: eventData.execution_time_ms ?? 0,
1836
+ tokenUsage: eventData.token_usage,
1837
+ contextWindow: eventData.context_window,
1838
+ } as ChatStreamEvent);
1839
+ } else if (eventData.token) {
1840
+ stream.emit("event", {
1841
+ type: "chunk",
1842
+ content: eventData.token,
1843
+ } as ChatStreamEvent);
1844
+ }
1845
+ } catch {
1846
+ // skip malformed SSE data
1847
+ }
1848
+ }
1849
+ stream.close();
1850
+ } catch (err: any) {
1851
+ stream.emit("event", {
1852
+ type: "error",
1853
+ error: err.message ?? String(err),
1854
+ } as ChatStreamEvent);
1855
+ stream.close();
1856
+ }
1857
+ })();
1858
+
1859
+ return stream;
1860
+ }
1861
+
1561
1862
  /**
1562
1863
  * Get a chat session by ID
1563
1864
  */
@@ -1942,6 +2243,517 @@ export class EkoDBClient {
1942
2243
  );
1943
2244
  }
1944
2245
 
2246
+ // ========================================================================
2247
+ // GOAL API
2248
+ // ========================================================================
2249
+
2250
+ /** Create a new goal */
2251
+ async goalCreate(data: Record): Promise<Record> {
2252
+ return this.makeRequest<Record>("POST", "/api/chat/goals", data, 0, true);
2253
+ }
2254
+
2255
+ /** List all goals */
2256
+ async goalList(): Promise<Record> {
2257
+ return this.makeRequest<Record>(
2258
+ "GET",
2259
+ "/api/chat/goals",
2260
+ undefined,
2261
+ 0,
2262
+ true,
2263
+ );
2264
+ }
2265
+
2266
+ /** Get a goal by ID */
2267
+ async goalGet(id: string): Promise<Record> {
2268
+ return this.makeRequest<Record>(
2269
+ "GET",
2270
+ `/api/chat/goals/${encodeURIComponent(id)}`,
2271
+ undefined,
2272
+ 0,
2273
+ true,
2274
+ );
2275
+ }
2276
+
2277
+ /** Update a goal by ID */
2278
+ async goalUpdate(id: string, data: Record): Promise<Record> {
2279
+ return this.makeRequest<Record>(
2280
+ "PUT",
2281
+ `/api/chat/goals/${encodeURIComponent(id)}`,
2282
+ data,
2283
+ 0,
2284
+ true,
2285
+ );
2286
+ }
2287
+
2288
+ /** Delete a goal by ID */
2289
+ async goalDelete(id: string): Promise<void> {
2290
+ await this.makeRequest<void>(
2291
+ "DELETE",
2292
+ `/api/chat/goals/${encodeURIComponent(id)}`,
2293
+ undefined,
2294
+ 0,
2295
+ true,
2296
+ );
2297
+ }
2298
+
2299
+ // ── Goal Templates ──
2300
+
2301
+ /** Create a new goal template */
2302
+ async goalTemplateCreate(data: Record): Promise<Record> {
2303
+ return this.makeRequest<Record>(
2304
+ "POST",
2305
+ "/api/chat/goal-templates",
2306
+ data,
2307
+ 0,
2308
+ true,
2309
+ );
2310
+ }
2311
+
2312
+ /** List all goal templates */
2313
+ async goalTemplateList(): Promise<Record> {
2314
+ return this.makeRequest<Record>(
2315
+ "GET",
2316
+ "/api/chat/goal-templates",
2317
+ undefined,
2318
+ 0,
2319
+ true,
2320
+ );
2321
+ }
2322
+
2323
+ /** Get a goal template by ID */
2324
+ async goalTemplateGet(id: string): Promise<Record> {
2325
+ return this.makeRequest<Record>(
2326
+ "GET",
2327
+ `/api/chat/goal-templates/${encodeURIComponent(id)}`,
2328
+ undefined,
2329
+ 0,
2330
+ true,
2331
+ );
2332
+ }
2333
+
2334
+ /** Update a goal template by ID */
2335
+ async goalTemplateUpdate(id: string, data: Record): Promise<Record> {
2336
+ return this.makeRequest<Record>(
2337
+ "PUT",
2338
+ `/api/chat/goal-templates/${encodeURIComponent(id)}`,
2339
+ data,
2340
+ 0,
2341
+ true,
2342
+ );
2343
+ }
2344
+
2345
+ /** Delete a goal template by ID */
2346
+ async goalTemplateDelete(id: string): Promise<void> {
2347
+ await this.makeRequest<void>(
2348
+ "DELETE",
2349
+ `/api/chat/goal-templates/${encodeURIComponent(id)}`,
2350
+ undefined,
2351
+ 0,
2352
+ true,
2353
+ );
2354
+ }
2355
+
2356
+ /** Search goals */
2357
+ async goalSearch(query: string): Promise<Record> {
2358
+ const params = new URLSearchParams({ q: query });
2359
+ return this.makeRequest<Record>(
2360
+ "GET",
2361
+ `/api/chat/goals/search?${params}`,
2362
+ undefined,
2363
+ 0,
2364
+ true,
2365
+ );
2366
+ }
2367
+
2368
+ /** Mark a goal as complete (status -> pending_review) */
2369
+ async goalComplete(id: string, data: Record): Promise<Record> {
2370
+ return this.makeRequest<Record>(
2371
+ "POST",
2372
+ `/api/chat/goals/${encodeURIComponent(id)}/complete`,
2373
+ data,
2374
+ 0,
2375
+ true,
2376
+ );
2377
+ }
2378
+
2379
+ /** Approve a goal (status -> in_progress) */
2380
+ async goalApprove(id: string): Promise<Record> {
2381
+ return this.makeRequest<Record>(
2382
+ "POST",
2383
+ `/api/chat/goals/${encodeURIComponent(id)}/approve`,
2384
+ undefined,
2385
+ 0,
2386
+ true,
2387
+ );
2388
+ }
2389
+
2390
+ /** Reject a goal (status -> failed) */
2391
+ async goalReject(id: string, data: Record): Promise<Record> {
2392
+ return this.makeRequest<Record>(
2393
+ "POST",
2394
+ `/api/chat/goals/${encodeURIComponent(id)}/reject`,
2395
+ data,
2396
+ 0,
2397
+ true,
2398
+ );
2399
+ }
2400
+
2401
+ /** Start a goal step (status -> in_progress) */
2402
+ async goalStepStart(id: string, stepIndex: number): Promise<Record> {
2403
+ return this.makeRequest<Record>(
2404
+ "POST",
2405
+ `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`,
2406
+ undefined,
2407
+ 0,
2408
+ true,
2409
+ );
2410
+ }
2411
+
2412
+ /** Complete a goal step with result */
2413
+ async goalStepComplete(
2414
+ id: string,
2415
+ stepIndex: number,
2416
+ data: Record,
2417
+ ): Promise<Record> {
2418
+ return this.makeRequest<Record>(
2419
+ "POST",
2420
+ `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`,
2421
+ data,
2422
+ 0,
2423
+ true,
2424
+ );
2425
+ }
2426
+
2427
+ /** Fail a goal step with error */
2428
+ async goalStepFail(
2429
+ id: string,
2430
+ stepIndex: number,
2431
+ data: Record,
2432
+ ): Promise<Record> {
2433
+ return this.makeRequest<Record>(
2434
+ "POST",
2435
+ `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`,
2436
+ data,
2437
+ 0,
2438
+ true,
2439
+ );
2440
+ }
2441
+
2442
+ // ========================================================================
2443
+ // TASK API
2444
+ // ========================================================================
2445
+
2446
+ /** Create a new scheduled task */
2447
+ async taskCreate(data: Record): Promise<Record> {
2448
+ return this.makeRequest<Record>("POST", "/api/chat/tasks", data, 0, true);
2449
+ }
2450
+
2451
+ /** List all scheduled tasks */
2452
+ async taskList(): Promise<Record> {
2453
+ return this.makeRequest<Record>(
2454
+ "GET",
2455
+ "/api/chat/tasks",
2456
+ undefined,
2457
+ 0,
2458
+ true,
2459
+ );
2460
+ }
2461
+
2462
+ /** Get a task by ID */
2463
+ async taskGet(id: string): Promise<Record> {
2464
+ return this.makeRequest<Record>(
2465
+ "GET",
2466
+ `/api/chat/tasks/${encodeURIComponent(id)}`,
2467
+ undefined,
2468
+ 0,
2469
+ true,
2470
+ );
2471
+ }
2472
+
2473
+ /** Update a task by ID */
2474
+ async taskUpdate(id: string, data: Record): Promise<Record> {
2475
+ return this.makeRequest<Record>(
2476
+ "PUT",
2477
+ `/api/chat/tasks/${encodeURIComponent(id)}`,
2478
+ data,
2479
+ 0,
2480
+ true,
2481
+ );
2482
+ }
2483
+
2484
+ /** Delete a task by ID */
2485
+ async taskDelete(id: string): Promise<void> {
2486
+ await this.makeRequest<void>(
2487
+ "DELETE",
2488
+ `/api/chat/tasks/${encodeURIComponent(id)}`,
2489
+ undefined,
2490
+ 0,
2491
+ true,
2492
+ );
2493
+ }
2494
+
2495
+ /** Get tasks that are due at the given time */
2496
+ async taskDue(now: string): Promise<Record> {
2497
+ const params = new URLSearchParams({ now });
2498
+ return this.makeRequest<Record>(
2499
+ "GET",
2500
+ `/api/chat/tasks/due?${params}`,
2501
+ undefined,
2502
+ 0,
2503
+ true,
2504
+ );
2505
+ }
2506
+
2507
+ /** Start a task (status -> running) */
2508
+ async taskStart(id: string): Promise<Record> {
2509
+ return this.makeRequest<Record>(
2510
+ "POST",
2511
+ `/api/chat/tasks/${encodeURIComponent(id)}/start`,
2512
+ undefined,
2513
+ 0,
2514
+ true,
2515
+ );
2516
+ }
2517
+
2518
+ /** Mark a task as succeeded */
2519
+ async taskSucceed(id: string, data: Record): Promise<Record> {
2520
+ return this.makeRequest<Record>(
2521
+ "POST",
2522
+ `/api/chat/tasks/${encodeURIComponent(id)}/succeed`,
2523
+ data,
2524
+ 0,
2525
+ true,
2526
+ );
2527
+ }
2528
+
2529
+ /** Mark a task as failed */
2530
+ async taskFail(id: string, data: Record): Promise<Record> {
2531
+ return this.makeRequest<Record>(
2532
+ "POST",
2533
+ `/api/chat/tasks/${encodeURIComponent(id)}/fail`,
2534
+ data,
2535
+ 0,
2536
+ true,
2537
+ );
2538
+ }
2539
+
2540
+ /** Pause a task */
2541
+ async taskPause(id: string): Promise<Record> {
2542
+ return this.makeRequest<Record>(
2543
+ "POST",
2544
+ `/api/chat/tasks/${encodeURIComponent(id)}/pause`,
2545
+ undefined,
2546
+ 0,
2547
+ true,
2548
+ );
2549
+ }
2550
+
2551
+ /** Resume a paused task */
2552
+ async taskResume(id: string, data: Record): Promise<Record> {
2553
+ return this.makeRequest<Record>(
2554
+ "POST",
2555
+ `/api/chat/tasks/${encodeURIComponent(id)}/resume`,
2556
+ data,
2557
+ 0,
2558
+ true,
2559
+ );
2560
+ }
2561
+
2562
+ // ========================================================================
2563
+ // AGENT API
2564
+ // ========================================================================
2565
+
2566
+ /** Create a new agent */
2567
+ async agentCreate(data: Record): Promise<Record> {
2568
+ return this.makeRequest<Record>("POST", "/api/chat/agents", data, 0, true);
2569
+ }
2570
+
2571
+ /** List all agents */
2572
+ async agentList(): Promise<Record> {
2573
+ return this.makeRequest<Record>(
2574
+ "GET",
2575
+ "/api/chat/agents",
2576
+ undefined,
2577
+ 0,
2578
+ true,
2579
+ );
2580
+ }
2581
+
2582
+ /** Get an agent by ID */
2583
+ async agentGet(id: string): Promise<Record> {
2584
+ return this.makeRequest<Record>(
2585
+ "GET",
2586
+ `/api/chat/agents/${encodeURIComponent(id)}`,
2587
+ undefined,
2588
+ 0,
2589
+ true,
2590
+ );
2591
+ }
2592
+
2593
+ /** Get an agent by name */
2594
+ async agentGetByName(name: string): Promise<Record> {
2595
+ return this.makeRequest<Record>(
2596
+ "GET",
2597
+ `/api/chat/agents/by-name/${encodeURIComponent(name)}`,
2598
+ undefined,
2599
+ 0,
2600
+ true,
2601
+ );
2602
+ }
2603
+
2604
+ /** Update an agent by ID */
2605
+ async agentUpdate(id: string, data: Record): Promise<Record> {
2606
+ return this.makeRequest<Record>(
2607
+ "PUT",
2608
+ `/api/chat/agents/${encodeURIComponent(id)}`,
2609
+ data,
2610
+ 0,
2611
+ true,
2612
+ );
2613
+ }
2614
+
2615
+ /** Delete an agent by ID */
2616
+ async agentDelete(id: string): Promise<void> {
2617
+ await this.makeRequest<void>(
2618
+ "DELETE",
2619
+ `/api/chat/agents/${encodeURIComponent(id)}`,
2620
+ undefined,
2621
+ 0,
2622
+ true,
2623
+ );
2624
+ }
2625
+
2626
+ /** Get agents by deployment ID */
2627
+ async agentsByDeployment(deploymentId: string): Promise<Record> {
2628
+ return this.makeRequest<Record>(
2629
+ "GET",
2630
+ `/api/chat/agents/by-deployment/${encodeURIComponent(deploymentId)}`,
2631
+ undefined,
2632
+ 0,
2633
+ true,
2634
+ );
2635
+ }
2636
+
2637
+ // ========================================================================
2638
+ // KV DOCUMENT LINKING
2639
+ // ========================================================================
2640
+
2641
+ /** Get documents linked to a KV key */
2642
+ async kvGetLinks(key: string): Promise<Record> {
2643
+ return this.makeRequest<Record>(
2644
+ "GET",
2645
+ `/api/kv/links/${encodeURIComponent(key)}`,
2646
+ undefined,
2647
+ 0,
2648
+ true,
2649
+ );
2650
+ }
2651
+
2652
+ /** Link a document to a KV key */
2653
+ async kvLink(
2654
+ key: string,
2655
+ collection: string,
2656
+ documentId: string,
2657
+ ): Promise<Record> {
2658
+ return this.makeRequest<Record>(
2659
+ "POST",
2660
+ `/api/kv/link`,
2661
+ { key, collection, document_id: documentId },
2662
+ 0,
2663
+ true,
2664
+ );
2665
+ }
2666
+
2667
+ /** Unlink a document from a KV key */
2668
+ async kvUnlink(
2669
+ key: string,
2670
+ collection: string,
2671
+ documentId: string,
2672
+ ): Promise<Record> {
2673
+ return this.makeRequest<Record>(
2674
+ "POST",
2675
+ `/api/kv/unlink`,
2676
+ { key, collection, document_id: documentId },
2677
+ 0,
2678
+ true,
2679
+ );
2680
+ }
2681
+
2682
+ // ========================================================================
2683
+ // SCHEDULE MANAGEMENT
2684
+ // ========================================================================
2685
+
2686
+ /** Create a new schedule */
2687
+ async createSchedule(data: Record): Promise<Record> {
2688
+ return this.makeRequest<Record>("POST", `/api/schedules`, data, 0, true);
2689
+ }
2690
+
2691
+ /** List all schedules */
2692
+ async listSchedules(): Promise<Record> {
2693
+ return this.makeRequest<Record>(
2694
+ "GET",
2695
+ `/api/schedules`,
2696
+ undefined,
2697
+ 0,
2698
+ true,
2699
+ );
2700
+ }
2701
+
2702
+ /** Get a schedule by ID */
2703
+ async getSchedule(id: string): Promise<Record> {
2704
+ return this.makeRequest<Record>(
2705
+ "GET",
2706
+ `/api/schedules/${encodeURIComponent(id)}`,
2707
+ undefined,
2708
+ 0,
2709
+ true,
2710
+ );
2711
+ }
2712
+
2713
+ /** Update a schedule */
2714
+ async updateSchedule(id: string, data: Record): Promise<Record> {
2715
+ return this.makeRequest<Record>(
2716
+ "PUT",
2717
+ `/api/schedules/${encodeURIComponent(id)}`,
2718
+ data,
2719
+ 0,
2720
+ true,
2721
+ );
2722
+ }
2723
+
2724
+ /** Delete a schedule */
2725
+ async deleteSchedule(id: string): Promise<void> {
2726
+ await this.makeRequest<void>(
2727
+ "DELETE",
2728
+ `/api/schedules/${encodeURIComponent(id)}`,
2729
+ undefined,
2730
+ 0,
2731
+ true,
2732
+ );
2733
+ }
2734
+
2735
+ /** Pause a schedule */
2736
+ async pauseSchedule(id: string): Promise<Record> {
2737
+ return this.makeRequest<Record>(
2738
+ "POST",
2739
+ `/api/schedules/${encodeURIComponent(id)}/pause`,
2740
+ undefined,
2741
+ 0,
2742
+ true,
2743
+ );
2744
+ }
2745
+
2746
+ /** Resume a schedule */
2747
+ async resumeSchedule(id: string): Promise<Record> {
2748
+ return this.makeRequest<Record>(
2749
+ "POST",
2750
+ `/api/schedules/${encodeURIComponent(id)}/resume`,
2751
+ undefined,
2752
+ 0,
2753
+ true,
2754
+ );
2755
+ }
2756
+
1945
2757
  // ========================================================================
1946
2758
  // COLLECTION UTILITIES
1947
2759
  // ========================================================================
@@ -2143,6 +2955,8 @@ export type ChatStreamEvent =
2143
2955
  tokenUsage?: any;
2144
2956
  toolCallHistory?: any;
2145
2957
  executionTimeMs: number;
2958
+ /** Model's context window size in tokens. */
2959
+ contextWindow?: number;
2146
2960
  }
2147
2961
  | {
2148
2962
  type: "toolCall";
@@ -2309,7 +3123,13 @@ export class WebSocketClient {
2309
3123
  switch (msg.type) {
2310
3124
  case "Success":
2311
3125
  case "Error": {
2312
- const messageId = msg.payload?.message_id || msg.payload?.messageId;
3126
+ // Try messageId from top-level, then from payload
3127
+ const messageId =
3128
+ msg.messageId ||
3129
+ msg.message_id ||
3130
+ msg.payload?.message_id ||
3131
+ msg.payload?.messageId;
3132
+ let matched = false;
2313
3133
  if (messageId && this.pendingRequests.has(messageId)) {
2314
3134
  const pending = this.pendingRequests.get(messageId)!;
2315
3135
  this.pendingRequests.delete(messageId);
@@ -2318,7 +3138,9 @@ export class WebSocketClient {
2318
3138
  } else {
2319
3139
  pending.resolve(msg.payload);
2320
3140
  }
2321
- } else if (this.registerToolsAck) {
3141
+ matched = true;
3142
+ }
3143
+ if (!matched && this.registerToolsAck) {
2322
3144
  const ack = this.registerToolsAck;
2323
3145
  this.registerToolsAck = null;
2324
3146
  if (msg.type === "Error") {
@@ -2326,6 +3148,20 @@ export class WebSocketClient {
2326
3148
  } else {
2327
3149
  ack.resolve(msg.payload);
2328
3150
  }
3151
+ matched = true;
3152
+ }
3153
+ // Server doesn't echo messageId — if there's exactly one pending
3154
+ // request, deliver the response to it (sequential request/response).
3155
+ if (!matched && this.pendingRequests.size === 1) {
3156
+ const entry = this.pendingRequests.entries().next().value!;
3157
+ const key = entry[0];
3158
+ const pending = entry[1];
3159
+ this.pendingRequests.delete(key);
3160
+ if (msg.type === "Error") {
3161
+ pending.reject(new Error(msg.message || "Unknown error"));
3162
+ } else {
3163
+ pending.resolve(msg.payload);
3164
+ }
2329
3165
  }
2330
3166
  break;
2331
3167
  }
@@ -2371,6 +3207,8 @@ export class WebSocketClient {
2371
3207
  msg.payload.tool_call_history || msg.payload.toolCallHistory,
2372
3208
  executionTimeMs:
2373
3209
  msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
3210
+ contextWindow:
3211
+ msg.payload.context_window || msg.payload.contextWindow,
2374
3212
  } as ChatStreamEvent);
2375
3213
  this.chatStreams.delete(chatId);
2376
3214
  stream.close();
@@ -2566,6 +3404,33 @@ export class WebSocketClient {
2566
3404
  this.ws.send(JSON.stringify(request));
2567
3405
  }
2568
3406
 
3407
+ /**
3408
+ * Stateless raw LLM completion via WebSocket.
3409
+ *
3410
+ * Sends a RawComplete message and waits for the Success response.
3411
+ * Preferred over HTTP for deployed instances: the persistent WSS
3412
+ * connection is already authenticated and won't be killed by reverse
3413
+ * proxy timeouts.
3414
+ */
3415
+ async rawCompletion(
3416
+ request: RawCompletionRequest,
3417
+ ): Promise<RawCompletionResponse> {
3418
+ await this.ensureConnected();
3419
+ const messageId = this.genMessageId();
3420
+ const payload = await this.sendRequest({
3421
+ type: "RawComplete",
3422
+ messageId,
3423
+ payload: {
3424
+ system_prompt: request.system_prompt,
3425
+ message: request.message,
3426
+ ...(request.provider && { provider: request.provider }),
3427
+ ...(request.model && { model: request.model }),
3428
+ ...(request.max_tokens != null && { max_tokens: request.max_tokens }),
3429
+ },
3430
+ });
3431
+ return { content: payload?.data?.content || "" };
3432
+ }
3433
+
2569
3434
  /**
2570
3435
  * Close the WebSocket connection.
2571
3436
  */