@babav/knowledge-core-client 0.29.0 → 0.31.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
@@ -95,7 +95,15 @@ export interface QueryRequest {
95
95
  * registers). */
96
96
  export type VisualLayout = "linear-flow" | "ring-cycle" | "nesting" | "radial-hub" | "side-by-side" | "stacked-layers" | "two-state" | "free";
97
97
  /** A visual attached to a response, anchored by char offsets into `answer` (SAME coordinate
98
- * system as citations). For delivery="svg", `rendered_svg` is set; scenic sets `image`. */
98
+ * system as citations).
99
+ *
100
+ * UNIFORM DELIVERY: every register (chart / structural / conceptual / scenic) carries `image` — a
101
+ * PNG served BY KC. To display it, GET `image.url` (resolve a relative path against your KC base
102
+ * URL) with your normal API key; the response body is the PNG bytes. The URL is a stable, non-
103
+ * expiring KC endpoint for persisted/conversational visuals (`expires_at` absent), so it works on
104
+ * reload/copy/export and on later conversation retrieval — the client never fetches GCS and never
105
+ * parses inline SVG. `rendered_svg` appears ONLY as a rare fail-soft fallback (server rasterization
106
+ * failed); prefer `image` and treat `rendered_svg` as a last resort. */
99
107
  export interface Visual {
100
108
  id: string;
101
109
  anchor: {
@@ -107,13 +115,15 @@ export interface Visual {
107
115
  payload: Record<string, unknown>;
108
116
  /** Declared spatial arrangement (meaningful for `conceptual`; "free" otherwise). */
109
117
  layout: VisualLayout;
110
- rendered_svg?: string;
118
+ /** PRIMARY: GET this (relative to your KC base, with your API key) -> PNG bytes. */
111
119
  image?: {
112
120
  url: string;
113
- expires_at: number;
114
121
  width: number;
115
122
  height: number;
123
+ expires_at?: number;
116
124
  };
125
+ /** Fail-soft fallback only (rasterize/store failed); normally absent. */
126
+ rendered_svg?: string;
117
127
  }
118
128
  /** Visual stage (streaming): generation has started. MAY be emitted twice — first with
119
129
  * `pending: null` (started, count unknown), then again with the ACCURATE post-vet count before
@@ -531,6 +541,16 @@ export declare class KnowledgeCoreClient extends HttpBase {
531
541
  * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
532
542
  * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
533
543
  queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
544
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
545
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
546
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
547
+ * New chat: const chat = kc.chat(agentId, corpusIds);
548
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
549
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
550
+ * sending leaves nothing behind). See ChatSession.send. */
551
+ chat(agentId: UUID, corpusIds: UUID[], opts?: {
552
+ conversationId?: UUID;
553
+ }): ChatSession;
534
554
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
535
555
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
536
556
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -896,3 +916,37 @@ export declare class AdminClient extends HttpBase {
896
916
  delete: (id: UUID) => Promise<void>;
897
917
  };
898
918
  }
919
+ export interface ChatSendOptions extends StreamHandlers {
920
+ overrides?: QueryRequest["overrides"];
921
+ filter?: QueryRequest["filter"];
922
+ visual?: QueryRequest["visual"];
923
+ /** Abort on unmount / navigation so the SSE connection doesn't linger. */
924
+ signal?: AbortSignal;
925
+ /** Applied ONLY when the conversation is lazily created on the FIRST message. `title` defaults
926
+ * to the message text (truncated) so a chat is never left untitled. */
927
+ title?: string;
928
+ customMetadata?: Record<string, unknown>;
929
+ ephemeral?: boolean;
930
+ /** Fired with the conversation id the moment it is created (first message) or resumed — use it to
931
+ * update the sidebar/URL/route IMMEDIATELY, before the answer streams. */
932
+ onConversationId?: (id: UUID) => void;
933
+ }
934
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
935
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
936
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
937
+ * a chat turn is never lost and a chat conversation is never left blank. */
938
+ export declare class ChatSession {
939
+ #private;
940
+ /** The conversation id — null until the first `send()` (unless resumed). */
941
+ conversationId: UUID | null;
942
+ constructor(kc: KnowledgeCoreClient, agentId: UUID, corpusIds: UUID[], conversationId: UUID | null);
943
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
944
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
945
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
946
+ send(text: string, o?: ChatSendOptions): Promise<void>;
947
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
948
+ messages(q?: {
949
+ limit?: number;
950
+ cursor?: string;
951
+ }): Promise<Page<Message>>;
952
+ }
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.31.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
@@ -129,7 +129,15 @@ export type VisualLayout =
129
129
  | "free";
130
130
 
131
131
  /** A visual attached to a response, anchored by char offsets into `answer` (SAME coordinate
132
- * system as citations). For delivery="svg", `rendered_svg` is set; scenic sets `image`. */
132
+ * system as citations).
133
+ *
134
+ * UNIFORM DELIVERY: every register (chart / structural / conceptual / scenic) carries `image` — a
135
+ * PNG served BY KC. To display it, GET `image.url` (resolve a relative path against your KC base
136
+ * URL) with your normal API key; the response body is the PNG bytes. The URL is a stable, non-
137
+ * expiring KC endpoint for persisted/conversational visuals (`expires_at` absent), so it works on
138
+ * reload/copy/export and on later conversation retrieval — the client never fetches GCS and never
139
+ * parses inline SVG. `rendered_svg` appears ONLY as a rare fail-soft fallback (server rasterization
140
+ * failed); prefer `image` and treat `rendered_svg` as a last resort. */
133
141
  export interface Visual {
134
142
  id: string;
135
143
  anchor: { start: number; end: number };
@@ -138,8 +146,10 @@ export interface Visual {
138
146
  payload: Record<string, unknown>; // {kind: "grammar"|"vega_lite"|"svg"|"image", ...}
139
147
  /** Declared spatial arrangement (meaningful for `conceptual`; "free" otherwise). */
140
148
  layout: VisualLayout;
149
+ /** PRIMARY: GET this (relative to your KC base, with your API key) -> PNG bytes. */
150
+ image?: { url: string; width: number; height: number; expires_at?: number };
151
+ /** Fail-soft fallback only (rasterize/store failed); normally absent. */
141
152
  rendered_svg?: string;
142
- image?: { url: string; expires_at: number; width: number; height: number };
143
153
  }
144
154
 
145
155
  /** Visual stage (streaming): generation has started. MAY be emitted twice — first with
@@ -645,6 +655,17 @@ export class KnowledgeCoreClient extends HttpBase {
645
655
  if (buf.trim()) dispatchSse(buf, handlers);
646
656
  }
647
657
 
658
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
659
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
660
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
661
+ * New chat: const chat = kc.chat(agentId, corpusIds);
662
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
663
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
664
+ * sending leaves nothing behind). See ChatSession.send. */
665
+ chat(agentId: UUID, corpusIds: UUID[], opts?: { conversationId?: UUID }): ChatSession {
666
+ return new ChatSession(this, agentId, corpusIds, opts?.conversationId ?? null);
667
+ }
668
+
648
669
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
649
670
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
650
671
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -1001,6 +1022,74 @@ export class AdminClient extends HttpBase {
1001
1022
  };
1002
1023
  }
1003
1024
 
1025
+ export interface ChatSendOptions extends StreamHandlers {
1026
+ overrides?: QueryRequest["overrides"];
1027
+ filter?: QueryRequest["filter"];
1028
+ visual?: QueryRequest["visual"];
1029
+ /** Abort on unmount / navigation so the SSE connection doesn't linger. */
1030
+ signal?: AbortSignal;
1031
+ /** Applied ONLY when the conversation is lazily created on the FIRST message. `title` defaults
1032
+ * to the message text (truncated) so a chat is never left untitled. */
1033
+ title?: string;
1034
+ customMetadata?: Record<string, unknown>;
1035
+ ephemeral?: boolean;
1036
+ /** Fired with the conversation id the moment it is created (first message) or resumed — use it to
1037
+ * update the sidebar/URL/route IMMEDIATELY, before the answer streams. */
1038
+ onConversationId?: (id: UUID) => void;
1039
+ }
1040
+
1041
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
1042
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
1043
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
1044
+ * a chat turn is never lost and a chat conversation is never left blank. */
1045
+ export class ChatSession {
1046
+ #kc: KnowledgeCoreClient;
1047
+ #agentId: UUID;
1048
+ #corpusIds: UUID[];
1049
+ #creating: Promise<UUID> | null = null;
1050
+ /** The conversation id — null until the first `send()` (unless resumed). */
1051
+ conversationId: UUID | null;
1052
+
1053
+ constructor(kc: KnowledgeCoreClient, agentId: UUID, corpusIds: UUID[], conversationId: UUID | null) {
1054
+ this.#kc = kc;
1055
+ this.#agentId = agentId;
1056
+ this.#corpusIds = corpusIds;
1057
+ this.conversationId = conversationId;
1058
+ }
1059
+
1060
+ /** Lazy create-once (concurrency-safe): the first send starts creation; a racing send awaits it. */
1061
+ #ensure(o: ChatSendOptions, text: string): Promise<UUID> {
1062
+ if (this.conversationId) return Promise.resolve(this.conversationId);
1063
+ if (!this.#creating) {
1064
+ this.#creating = this.#kc.conversations
1065
+ .create({ title: o.title ?? text.slice(0, 80), custom_metadata: o.customMetadata, ephemeral: o.ephemeral })
1066
+ .then((c) => { this.conversationId = c.id; return c.id; });
1067
+ }
1068
+ return this.#creating;
1069
+ }
1070
+
1071
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
1072
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
1073
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
1074
+ async send(text: string, o: ChatSendOptions = {}): Promise<void> {
1075
+ const convId = await this.#ensure(o, text);
1076
+ o.onConversationId?.(convId);
1077
+ await this.#kc.queryStream(
1078
+ this.#agentId,
1079
+ { corpus_ids: this.#corpusIds, query: text, conversation_id: convId,
1080
+ overrides: o.overrides, filter: o.filter, visual: o.visual },
1081
+ o,
1082
+ o.signal,
1083
+ );
1084
+ }
1085
+
1086
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
1087
+ messages(q?: { limit?: number; cursor?: string }): Promise<Page<Message>> {
1088
+ if (!this.conversationId) return Promise.resolve({ items: [], next_cursor: null });
1089
+ return this.#kc.conversations.listMessages(this.conversationId, q);
1090
+ }
1091
+ }
1092
+
1004
1093
  // ---------------------------------------------------------------------------
1005
1094
  // helpers
1006
1095
  // ---------------------------------------------------------------------------