@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/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 the current authentication token.
448
- * Returns null if not yet authenticated. Call refreshToken() first.
449
- */
450
- getToken(): string | null {
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
- const messageId = msg.payload?.message_id || msg.payload?.messageId;
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
- } else if (this.registerToolsAck) {
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
  */