@babav/knowledge-core-client 0.29.0 → 0.30.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/index.d.ts CHANGED
@@ -531,6 +531,16 @@ export declare class KnowledgeCoreClient extends HttpBase {
531
531
  * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
532
532
  * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
533
533
  queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
534
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
535
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
536
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
537
+ * New chat: const chat = kc.chat(agentId, corpusIds);
538
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
539
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
540
+ * sending leaves nothing behind). See ChatSession.send. */
541
+ chat(agentId: UUID, corpusIds: UUID[], opts?: {
542
+ conversationId?: UUID;
543
+ }): ChatSession;
534
544
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
535
545
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
536
546
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -896,3 +906,37 @@ export declare class AdminClient extends HttpBase {
896
906
  delete: (id: UUID) => Promise<void>;
897
907
  };
898
908
  }
909
+ export interface ChatSendOptions extends StreamHandlers {
910
+ overrides?: QueryRequest["overrides"];
911
+ filter?: QueryRequest["filter"];
912
+ visual?: QueryRequest["visual"];
913
+ /** Abort on unmount / navigation so the SSE connection doesn't linger. */
914
+ signal?: AbortSignal;
915
+ /** Applied ONLY when the conversation is lazily created on the FIRST message. `title` defaults
916
+ * to the message text (truncated) so a chat is never left untitled. */
917
+ title?: string;
918
+ customMetadata?: Record<string, unknown>;
919
+ ephemeral?: boolean;
920
+ /** Fired with the conversation id the moment it is created (first message) or resumed — use it to
921
+ * update the sidebar/URL/route IMMEDIATELY, before the answer streams. */
922
+ onConversationId?: (id: UUID) => void;
923
+ }
924
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
925
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
926
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
927
+ * a chat turn is never lost and a chat conversation is never left blank. */
928
+ export declare class ChatSession {
929
+ #private;
930
+ /** The conversation id — null until the first `send()` (unless resumed). */
931
+ conversationId: UUID | null;
932
+ constructor(kc: KnowledgeCoreClient, agentId: UUID, corpusIds: UUID[], conversationId: UUID | null);
933
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
934
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
935
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
936
+ send(text: string, o?: ChatSendOptions): Promise<void>;
937
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
938
+ messages(q?: {
939
+ limit?: number;
940
+ cursor?: string;
941
+ }): Promise<Page<Message>>;
942
+ }
package/dist/index.js CHANGED
@@ -178,6 +178,16 @@ export class KnowledgeCoreClient extends HttpBase {
178
178
  if (buf.trim())
179
179
  dispatchSse(buf, handlers);
180
180
  }
181
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
182
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
183
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
184
+ * New chat: const chat = kc.chat(agentId, corpusIds);
185
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
186
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
187
+ * sending leaves nothing behind). See ChatSession.send. */
188
+ chat(agentId, corpusIds, opts) {
189
+ return new ChatSession(this, agentId, corpusIds, opts?.conversationId ?? null);
190
+ }
181
191
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
182
192
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
183
193
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -460,6 +470,50 @@ export class AdminClient extends HttpBase {
460
470
  delete: (id) => this.request("DELETE", `/v1/agents/${id}`),
461
471
  };
462
472
  }
473
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
474
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
475
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
476
+ * a chat turn is never lost and a chat conversation is never left blank. */
477
+ export class ChatSession {
478
+ #kc;
479
+ #agentId;
480
+ #corpusIds;
481
+ #creating = null;
482
+ /** The conversation id — null until the first `send()` (unless resumed). */
483
+ conversationId;
484
+ constructor(kc, agentId, corpusIds, conversationId) {
485
+ this.#kc = kc;
486
+ this.#agentId = agentId;
487
+ this.#corpusIds = corpusIds;
488
+ this.conversationId = conversationId;
489
+ }
490
+ /** Lazy create-once (concurrency-safe): the first send starts creation; a racing send awaits it. */
491
+ #ensure(o, text) {
492
+ if (this.conversationId)
493
+ return Promise.resolve(this.conversationId);
494
+ if (!this.#creating) {
495
+ this.#creating = this.#kc.conversations
496
+ .create({ title: o.title ?? text.slice(0, 80), custom_metadata: o.customMetadata, ephemeral: o.ephemeral })
497
+ .then((c) => { this.conversationId = c.id; return c.id; });
498
+ }
499
+ return this.#creating;
500
+ }
501
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
502
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
503
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
504
+ async send(text, o = {}) {
505
+ const convId = await this.#ensure(o, text);
506
+ o.onConversationId?.(convId);
507
+ await this.#kc.queryStream(this.#agentId, { corpus_ids: this.#corpusIds, query: text, conversation_id: convId,
508
+ overrides: o.overrides, filter: o.filter, visual: o.visual }, o, o.signal);
509
+ }
510
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
511
+ messages(q) {
512
+ if (!this.conversationId)
513
+ return Promise.resolve({ items: [], next_cursor: null });
514
+ return this.#kc.conversations.listMessages(this.conversationId, q);
515
+ }
516
+ }
463
517
  // ---------------------------------------------------------------------------
464
518
  // helpers
465
519
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babav/knowledge-core-client",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "TypeScript client for the Babav Knowledge Core API (Deno + Node 18+, zero deps). Includes the babav.visual grammar TYPES at the ./visual subpath (types only; all visual rendering is server-side).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -645,6 +645,17 @@ export class KnowledgeCoreClient extends HttpBase {
645
645
  if (buf.trim()) dispatchSse(buf, handlers);
646
646
  }
647
647
 
648
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
649
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
650
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
651
+ * New chat: const chat = kc.chat(agentId, corpusIds);
652
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
653
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
654
+ * sending leaves nothing behind). See ChatSession.send. */
655
+ chat(agentId: UUID, corpusIds: UUID[], opts?: { conversationId?: UUID }): ChatSession {
656
+ return new ChatSession(this, agentId, corpusIds, opts?.conversationId ?? null);
657
+ }
658
+
648
659
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
649
660
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
650
661
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -1001,6 +1012,74 @@ export class AdminClient extends HttpBase {
1001
1012
  };
1002
1013
  }
1003
1014
 
1015
+ export interface ChatSendOptions extends StreamHandlers {
1016
+ overrides?: QueryRequest["overrides"];
1017
+ filter?: QueryRequest["filter"];
1018
+ visual?: QueryRequest["visual"];
1019
+ /** Abort on unmount / navigation so the SSE connection doesn't linger. */
1020
+ signal?: AbortSignal;
1021
+ /** Applied ONLY when the conversation is lazily created on the FIRST message. `title` defaults
1022
+ * to the message text (truncated) so a chat is never left untitled. */
1023
+ title?: string;
1024
+ customMetadata?: Record<string, unknown>;
1025
+ ephemeral?: boolean;
1026
+ /** Fired with the conversation id the moment it is created (first message) or resumed — use it to
1027
+ * update the sidebar/URL/route IMMEDIATELY, before the answer streams. */
1028
+ onConversationId?: (id: UUID) => void;
1029
+ }
1030
+
1031
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
1032
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
1033
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
1034
+ * a chat turn is never lost and a chat conversation is never left blank. */
1035
+ export class ChatSession {
1036
+ #kc: KnowledgeCoreClient;
1037
+ #agentId: UUID;
1038
+ #corpusIds: UUID[];
1039
+ #creating: Promise<UUID> | null = null;
1040
+ /** The conversation id — null until the first `send()` (unless resumed). */
1041
+ conversationId: UUID | null;
1042
+
1043
+ constructor(kc: KnowledgeCoreClient, agentId: UUID, corpusIds: UUID[], conversationId: UUID | null) {
1044
+ this.#kc = kc;
1045
+ this.#agentId = agentId;
1046
+ this.#corpusIds = corpusIds;
1047
+ this.conversationId = conversationId;
1048
+ }
1049
+
1050
+ /** Lazy create-once (concurrency-safe): the first send starts creation; a racing send awaits it. */
1051
+ #ensure(o: ChatSendOptions, text: string): Promise<UUID> {
1052
+ if (this.conversationId) return Promise.resolve(this.conversationId);
1053
+ if (!this.#creating) {
1054
+ this.#creating = this.#kc.conversations
1055
+ .create({ title: o.title ?? text.slice(0, 80), custom_metadata: o.customMetadata, ephemeral: o.ephemeral })
1056
+ .then((c) => { this.conversationId = c.id; return c.id; });
1057
+ }
1058
+ return this.#creating;
1059
+ }
1060
+
1061
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
1062
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
1063
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
1064
+ async send(text: string, o: ChatSendOptions = {}): Promise<void> {
1065
+ const convId = await this.#ensure(o, text);
1066
+ o.onConversationId?.(convId);
1067
+ await this.#kc.queryStream(
1068
+ this.#agentId,
1069
+ { corpus_ids: this.#corpusIds, query: text, conversation_id: convId,
1070
+ overrides: o.overrides, filter: o.filter, visual: o.visual },
1071
+ o,
1072
+ o.signal,
1073
+ );
1074
+ }
1075
+
1076
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
1077
+ messages(q?: { limit?: number; cursor?: string }): Promise<Page<Message>> {
1078
+ if (!this.conversationId) return Promise.resolve({ items: [], next_cursor: null });
1079
+ return this.#kc.conversations.listMessages(this.conversationId, q);
1080
+ }
1081
+ }
1082
+
1004
1083
  // ---------------------------------------------------------------------------
1005
1084
  // helpers
1006
1085
  // ---------------------------------------------------------------------------