@babav/knowledge-core-client 0.1.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/index.ts ADDED
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Babav Knowledge Core — TypeScript client.
3
+ *
4
+ * Portable: uses the global `fetch` + `ReadableStream`, so it runs unchanged in
5
+ * Supabase Edge Functions (Deno) and Railway (Node 18+). Zero dependencies.
6
+ *
7
+ * Wire shapes are snake_case to mirror the Knowledge Core API 1:1 — so an API
8
+ * change is reflected here with a one-to-one type edit and never drifts.
9
+ *
10
+ * Auth: every call carries an X-API-Key. Use TenantClient with a TENANT key for
11
+ * all data ops; use AdminClient with the ADMIN key for tenant / API-key / agent
12
+ * management. The key is server-side only — never ship it to a browser.
13
+ */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Shared types (mirror the API Read models)
17
+ // ---------------------------------------------------------------------------
18
+ export type UUID = string;
19
+
20
+ export interface Page<T> {
21
+ items: T[];
22
+ next_cursor: string | null;
23
+ }
24
+
25
+ export type Visibility = "downloadable" | "viewable" | "attributable" | "hidden";
26
+ export type FilterOp = "eq" | "ne" | "in" | "nin" | "gt" | "gte" | "lt" | "lte" | "exists";
27
+
28
+ export interface FilterClause {
29
+ field: string;
30
+ op: FilterOp;
31
+ value?: unknown;
32
+ }
33
+ export interface MetadataFilter {
34
+ op?: "and" | "or";
35
+ clauses: FilterClause[];
36
+ }
37
+
38
+ export interface RetrievalContent {
39
+ text: string;
40
+ score: number;
41
+ parent_id: string;
42
+ document_id: string;
43
+ corpus_id: string;
44
+ source_type: "corpus" | "conversation_attachment";
45
+ visibility: Visibility | string;
46
+ metadata: Record<string, unknown>;
47
+ source_url: string | null;
48
+ }
49
+
50
+ export interface QueryOverrides {
51
+ generation_model?: string;
52
+ top_k?: number;
53
+ rerank_instruction?: string;
54
+ max_hops?: number;
55
+ groundedness_threshold?: number;
56
+ grounding_enabled?: boolean;
57
+ }
58
+
59
+ export interface QueryRequest {
60
+ corpus_ids: UUID[];
61
+ query: string;
62
+ conversation_id?: UUID | null; // present => conversational; absent => one-shot
63
+ overrides?: QueryOverrides;
64
+ filter?: MetadataFilter;
65
+ }
66
+
67
+ export interface QueryResponse {
68
+ conversation_id: UUID | null;
69
+ message_id: UUID | null;
70
+ answer: string;
71
+ retrieval_contents: RetrievalContent[];
72
+ citations: Citation[];
73
+ groundedness: Groundedness | null;
74
+ usage: Record<string, number | null>;
75
+ standalone_query: string | null;
76
+ hops: string[];
77
+ }
78
+
79
+ export interface Citation {
80
+ quote: string;
81
+ text_span: [number, number] | null;
82
+ context_index: number;
83
+ document_id: string;
84
+ source_url: string | null;
85
+ supported: boolean;
86
+ }
87
+ export interface Groundedness {
88
+ score: number;
89
+ model: string;
90
+ threshold: number | null;
91
+ passed: boolean | null;
92
+ }
93
+
94
+ export interface Corpus {
95
+ id: UUID;
96
+ tenant_id: UUID | null;
97
+ name: string;
98
+ description: string | null;
99
+ embedding_model: string;
100
+ external_ref: string | null;
101
+ created_at: string;
102
+ updated_at: string;
103
+ }
104
+ export interface Folder {
105
+ id: UUID;
106
+ corpus_id: UUID;
107
+ name: string;
108
+ is_default: boolean;
109
+ created_at: string;
110
+ updated_at: string;
111
+ }
112
+ export interface Document {
113
+ id: UUID;
114
+ corpus_id: UUID;
115
+ folder_id: UUID;
116
+ source_uri: string;
117
+ status: string;
118
+ visibility: Visibility;
119
+ content_type: string | null;
120
+ filename: string | null;
121
+ parse_job_id: UUID | null;
122
+ chunk_count: number;
123
+ datapoint_ids: string[];
124
+ custom_metadata: Record<string, unknown>;
125
+ error: string | null;
126
+ created_at: string;
127
+ updated_at: string;
128
+ }
129
+ export interface ContentUrl {
130
+ content_url: string;
131
+ content_type: string;
132
+ filename: string;
133
+ }
134
+ export interface Conversation {
135
+ id: UUID;
136
+ tenant_id: UUID | null;
137
+ title: string | null;
138
+ custom_metadata: Record<string, unknown>;
139
+ ephemeral: boolean;
140
+ last_activity: string;
141
+ created_at: string;
142
+ updated_at: string;
143
+ }
144
+ export interface Message {
145
+ id: UUID;
146
+ conversation_id: UUID;
147
+ agent_id: UUID | null;
148
+ corpus_ids: UUID[] | null;
149
+ query: string;
150
+ answer: string | null;
151
+ citations: Record<string, unknown> | null;
152
+ groundedness: Record<string, unknown> | null;
153
+ retrieval_contents: unknown[] | null;
154
+ retrieval_debug: Record<string, unknown> | null;
155
+ usage: Record<string, unknown> | null;
156
+ created_at: string;
157
+ }
158
+ export interface ConversationDocument {
159
+ id: UUID;
160
+ conversation_id: UUID;
161
+ tenant_id: UUID;
162
+ filename: string | null;
163
+ content_type: string | null;
164
+ byte_size: number | null;
165
+ status: "parsing" | "ready" | "failed";
166
+ custom_metadata: Record<string, unknown> | null;
167
+ error: string | null;
168
+ created_at: string;
169
+ updated_at: string;
170
+ }
171
+ export interface ParseJob {
172
+ id: UUID;
173
+ tenant_id: UUID | null;
174
+ status: string;
175
+ document_count: number;
176
+ completed_count: number;
177
+ failed_count: number;
178
+ documents: unknown[];
179
+ created_at: string;
180
+ updated_at: string;
181
+ }
182
+ export interface Feedback {
183
+ id: UUID;
184
+ message_id: UUID;
185
+ rating: number | null;
186
+ comment: string | null;
187
+ created_at: string;
188
+ updated_at: string;
189
+ }
190
+ export interface Agent {
191
+ id: UUID;
192
+ name: string;
193
+ kind: "query" | "ingestion";
194
+ tenant_id: UUID | null;
195
+ generation_model: string | null;
196
+ top_k: number | null;
197
+ rerank_instruction: string | null;
198
+ max_hops: number | null;
199
+ groundedness_threshold: number | null;
200
+ grounding_enabled: boolean | null;
201
+ created_at: string;
202
+ updated_at: string;
203
+ }
204
+ export interface Tenant {
205
+ id: UUID;
206
+ name: string;
207
+ external_ref: string | null;
208
+ vector_index: string | null;
209
+ source_bucket: string | null;
210
+ artifacts_bucket: string | null;
211
+ query_endpoint: string | null;
212
+ query_public_domain: string | null;
213
+ query_deployed_index_id: string | null;
214
+ created_at: string;
215
+ updated_at: string;
216
+ }
217
+ export interface ApiKeyCreated {
218
+ id: UUID;
219
+ api_key: string; // shown ONCE — store it now
220
+ prefix: string;
221
+ label: string | null;
222
+ created_at: string;
223
+ }
224
+ export interface ApiKeyInfo {
225
+ id: UUID;
226
+ tenant_id: UUID;
227
+ prefix: string;
228
+ label: string | null;
229
+ last_used_at: string | null;
230
+ revoked_at: string | null;
231
+ created_at: string;
232
+ updated_at: string;
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Errors
237
+ // ---------------------------------------------------------------------------
238
+ /** Thrown on any non-2xx response. `detail` is the structured body when present
239
+ * (e.g. {error:"attachments_pending", attachments:[...]} or
240
+ * {error:"attachment_exceeds_context_window", ...}). */
241
+ export class KnowledgeCoreError extends Error {
242
+ constructor(
243
+ readonly status: number,
244
+ readonly detail: unknown,
245
+ readonly path: string,
246
+ ) {
247
+ super(`KnowledgeCore ${status} on ${path}: ${JSON.stringify(detail)}`);
248
+ this.name = "KnowledgeCoreError";
249
+ }
250
+ /** Convenience: the `error` code for structured guard responses. */
251
+ get code(): string | undefined {
252
+ const d = this.detail as { detail?: { error?: string }; error?: string };
253
+ return d?.detail?.error ?? d?.error;
254
+ }
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Base HTTP
259
+ // ---------------------------------------------------------------------------
260
+ export interface ClientOptions {
261
+ baseUrl: string;
262
+ apiKey: string;
263
+ /** Optional default fetch timeout (ms). Streaming ignores this. */
264
+ timeoutMs?: number;
265
+ fetch?: typeof fetch; // override for tests
266
+ }
267
+
268
+ interface RequestOpts {
269
+ query?: Record<string, string | number | boolean | undefined>;
270
+ json?: unknown;
271
+ body?: BodyInit; // raw body (attachment upload)
272
+ headers?: Record<string, string>;
273
+ signal?: AbortSignal;
274
+ }
275
+
276
+ class HttpBase {
277
+ protected readonly baseUrl: string;
278
+ protected readonly apiKey: string;
279
+ protected readonly timeoutMs?: number;
280
+ protected readonly _fetch: typeof fetch;
281
+
282
+ constructor(opts: ClientOptions) {
283
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
284
+ this.apiKey = opts.apiKey;
285
+ this.timeoutMs = opts.timeoutMs;
286
+ this._fetch = opts.fetch ?? fetch;
287
+ }
288
+
289
+ protected url(path: string, query?: RequestOpts["query"]): string {
290
+ const u = new URL(this.baseUrl + path);
291
+ if (query) {
292
+ for (const [k, v] of Object.entries(query)) {
293
+ if (v !== undefined) u.searchParams.set(k, String(v));
294
+ }
295
+ }
296
+ return u.toString();
297
+ }
298
+
299
+ protected async raw(method: string, path: string, opts: RequestOpts = {}): Promise<Response> {
300
+ const headers: Record<string, string> = { "x-api-key": this.apiKey, ...(opts.headers ?? {}) };
301
+ let body: BodyInit | undefined;
302
+ if (opts.json !== undefined) {
303
+ headers["content-type"] = "application/json";
304
+ body = JSON.stringify(opts.json);
305
+ } else if (opts.body !== undefined) {
306
+ body = opts.body;
307
+ }
308
+ let signal = opts.signal;
309
+ let timer: ReturnType<typeof setTimeout> | undefined;
310
+ if (!signal && this.timeoutMs) {
311
+ const ac = new AbortController();
312
+ timer = setTimeout(() => ac.abort(), this.timeoutMs);
313
+ signal = ac.signal;
314
+ }
315
+ try {
316
+ return await this._fetch(this.url(path, opts.query), { method, headers, body, signal });
317
+ } finally {
318
+ if (timer) clearTimeout(timer);
319
+ }
320
+ }
321
+
322
+ protected async request<T>(method: string, path: string, opts: RequestOpts = {}): Promise<T> {
323
+ const res = await this.raw(method, path, opts);
324
+ const text = await res.text();
325
+ const parsed = text ? safeJson(text) : null;
326
+ if (!res.ok) throw new KnowledgeCoreError(res.status, parsed ?? text, path);
327
+ return parsed as T;
328
+ }
329
+
330
+ /** Auto-paginate a list endpoint into a single array. */
331
+ protected async pageAll<T>(path: string, query: RequestOpts["query"] = {}): Promise<T[]> {
332
+ const out: T[] = [];
333
+ let cursor: string | null | undefined = undefined;
334
+ do {
335
+ const page: Page<T> = await this.request<Page<T>>("GET", path, {
336
+ query: { ...query, cursor: cursor ?? undefined },
337
+ });
338
+ out.push(...page.items);
339
+ cursor = page.next_cursor;
340
+ } while (cursor);
341
+ return out;
342
+ }
343
+ }
344
+
345
+ function safeJson(t: string): unknown {
346
+ try {
347
+ return JSON.parse(t);
348
+ } catch {
349
+ return t;
350
+ }
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // SSE handlers
355
+ // ---------------------------------------------------------------------------
356
+ export interface StreamHandlers {
357
+ onHop?: (d: { n: number; subquery: string; status: string; retrieval_contents?: number }) => void;
358
+ onSources?: (d: { retrieval_contents: RetrievalContent[] }) => void;
359
+ onToken?: (text: string) => void;
360
+ onFinal?: (d: QueryResponse) => void;
361
+ onError?: (d: unknown) => void;
362
+ onDone?: () => void;
363
+ /** Catch-all for any event (incl. unknown ones). */
364
+ onEvent?: (event: string, data: unknown) => void;
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Tenant client (data ops) — use a TENANT key
369
+ // ---------------------------------------------------------------------------
370
+ export class KnowledgeCoreClient extends HttpBase {
371
+ // --- query (agent-anchored) ---
372
+ query(agentId: UUID, body: QueryRequest): Promise<QueryResponse> {
373
+ return this.request("POST", `/v1/agents/${agentId}/query`, { json: body });
374
+ }
375
+
376
+ /** Streaming query (SSE). Resolves when the stream ends. */
377
+ async queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void> {
378
+ const res = await this.raw("POST", `/v1/agents/${agentId}/query/stream`, { json: body, signal });
379
+ if (!res.ok || !res.body) {
380
+ const t = await res.text();
381
+ throw new KnowledgeCoreError(res.status, safeJson(t), `/v1/agents/${agentId}/query/stream`);
382
+ }
383
+ const reader = res.body.getReader();
384
+ const decoder = new TextDecoder();
385
+ let buf = "";
386
+ for (;;) {
387
+ const { value, done } = await reader.read();
388
+ if (done) break;
389
+ buf += decoder.decode(value, { stream: true });
390
+ let idx: number;
391
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
392
+ const frame = buf.slice(0, idx);
393
+ buf = buf.slice(idx + 2);
394
+ dispatchSse(frame, handlers);
395
+ }
396
+ }
397
+ if (buf.trim()) dispatchSse(buf, handlers);
398
+ }
399
+
400
+ // --- retrieve (cross-corpus primitive, no generation) ---
401
+ retrieve(body: { query: string; corpus_ids: UUID[]; top_k?: number; rerank?: boolean; instruction?: string; filter?: MetadataFilter; }): Promise<{ retrieval_contents: RetrievalContent[] }> {
402
+ return this.request("POST", "/v1/retrieve", { json: body });
403
+ }
404
+
405
+ // --- corpora ---
406
+ corpora = {
407
+ create: (b: { name: string; embedding_model: string; description?: string; external_ref?: string }) =>
408
+ this.request<Corpus>("POST", "/v1/corpora", { json: b }),
409
+ list: (q?: { limit?: number; cursor?: string }) => this.request<Page<Corpus>>("GET", "/v1/corpora", { query: q }),
410
+ listAll: () => this.pageAll<Corpus>("/v1/corpora"),
411
+ get: (id: UUID) => this.request<Corpus>("GET", `/v1/corpora/${id}`),
412
+ update: (id: UUID, b: { name?: string; description?: string; external_ref?: string }) =>
413
+ this.request<Corpus>("PATCH", `/v1/corpora/${id}`, { json: b }),
414
+ delete: (id: UUID, confirmName: string) => this.request<void>("DELETE", `/v1/corpora/${id}`, { query: { confirm: confirmName } }),
415
+ listFolders: (id: UUID) => this.pageAll<Folder>(`/v1/corpora/${id}/folders`),
416
+ listDocuments: (id: UUID, q?: { limit?: number; cursor?: string }) =>
417
+ this.request<Page<Document>>("GET", `/v1/corpora/${id}/documents`, { query: q }),
418
+ createDocument: (id: UUID, b: { source_uri: string; folder_id?: UUID; visibility?: Visibility; status?: string; parse_job_id?: UUID; custom_metadata?: Record<string, unknown> }) =>
419
+ this.request<Document>("POST", `/v1/corpora/${id}/documents`, { json: b }),
420
+ ingest: (id: UUID, b: { ingestion_agent_id: UUID; folder_id?: UUID; documents: { gcs_uri: string; custom_metadata?: Record<string, unknown>; visibility?: Visibility }[] }) =>
421
+ this.request<{ parse_job_id: UUID }>("POST", `/v1/corpora/${id}/ingest`, { json: b }),
422
+ };
423
+
424
+ // --- folders ---
425
+ folders = {
426
+ create: (corpusId: UUID, name: string) => this.request<Folder>("POST", `/v1/corpora/${corpusId}/folders`, { json: { name } }),
427
+ rename: (folderId: UUID, name: string) => this.request<Folder>("PATCH", `/v1/folders/${folderId}`, { json: { name } }),
428
+ delete: (folderId: UUID) => this.request<void>("DELETE", `/v1/folders/${folderId}`),
429
+ listDocuments: (folderId: UUID, q?: { limit?: number; cursor?: string }) =>
430
+ this.request<Page<Document>>("GET", `/v1/folders/${folderId}/documents`, { query: q }),
431
+ };
432
+
433
+ // --- documents ---
434
+ documents = {
435
+ get: (id: UUID) => this.request<Document>("GET", `/v1/documents/${id}`),
436
+ getMetadata: (id: UUID) => this.request<{ document_id: UUID; custom_metadata: Record<string, unknown> }>("GET", `/v1/documents/${id}/metadata`),
437
+ patchMetadata: (id: UUID, custom_metadata: Record<string, unknown>) =>
438
+ this.request<{ document_id: UUID; custom_metadata: Record<string, unknown> }>("PATCH", `/v1/documents/${id}/metadata`, { json: { custom_metadata } }),
439
+ update: (id: UUID, b: { visibility?: Visibility; folder_id?: UUID }) => this.request<Document>("PATCH", `/v1/documents/${id}`, { json: b }),
440
+ contentUrl: (id: UUID, disposition: "inline" | "attachment" = "inline") =>
441
+ this.request<ContentUrl>("GET", `/v1/documents/${id}/content-url`, { query: { disposition } }),
442
+ delete: (id: UUID) => this.request<void>("DELETE", `/v1/documents/${id}`),
443
+ };
444
+
445
+ // --- conversations ---
446
+ conversations = {
447
+ create: (b?: { title?: string; custom_metadata?: Record<string, unknown>; ephemeral?: boolean }) =>
448
+ this.request<Conversation>("POST", "/v1/conversations", { json: b ?? {} }),
449
+ list: (q?: { limit?: number; cursor?: string }) => this.request<Page<Conversation>>("GET", "/v1/conversations", { query: q }),
450
+ get: (id: UUID) => this.request<Conversation>("GET", `/v1/conversations/${id}`),
451
+ update: (id: UUID, b: { title?: string; custom_metadata?: Record<string, unknown> }) =>
452
+ this.request<Conversation>("PATCH", `/v1/conversations/${id}`, { json: b }),
453
+ delete: (id: UUID) => this.request<void>("DELETE", `/v1/conversations/${id}`),
454
+ /** Filter by custom_metadata using the same metadata filter as documents. */
455
+ search: (b: { filter?: MetadataFilter; limit?: number; cursor?: string }) =>
456
+ this.request<Page<Conversation>>("POST", "/v1/conversations/search", { json: b }),
457
+ listMessages: (id: UUID, q?: { limit?: number; cursor?: string }) =>
458
+ this.request<Page<Message>>("GET", `/v1/conversations/${id}/messages`, { query: q }),
459
+ };
460
+
461
+ // --- conversation attachments (pinned full-document review) ---
462
+ attachments = {
463
+ /** Upload + attach a document. `data` is the raw file bytes. Returns status=parsing. */
464
+ upload: (conversationId: UUID, a: { filename: string; content_type: string; data: BodyInit; custom_metadata?: Record<string, unknown> }) =>
465
+ this.request<ConversationDocument>("POST", `/v1/conversations/${conversationId}/documents`, {
466
+ body: a.data,
467
+ headers: { "content-type": a.content_type },
468
+ query: { filename: a.filename, content_type: a.content_type, custom_metadata: a.custom_metadata ? JSON.stringify(a.custom_metadata) : undefined },
469
+ }),
470
+ list: (conversationId: UUID, q?: { limit?: number; cursor?: string }) =>
471
+ this.request<Page<ConversationDocument>>("GET", `/v1/conversations/${conversationId}/documents`, { query: q }),
472
+ get: (conversationId: UUID, docId: UUID) => this.request<ConversationDocument>("GET", `/v1/conversations/${conversationId}/documents/${docId}`),
473
+ contentUrl: (conversationId: UUID, docId: UUID, disposition: "inline" | "attachment" = "inline") =>
474
+ this.request<ContentUrl>("GET", `/v1/conversations/${conversationId}/documents/${docId}/content-url`, { query: { disposition } }),
475
+ delete: (conversationId: UUID, docId: UUID) => this.request<void>("DELETE", `/v1/conversations/${conversationId}/documents/${docId}`),
476
+ /** Poll until the attachment is ready or failed (or timeout). */
477
+ waitReady: async (conversationId: UUID, docId: UUID, opts: { intervalMs?: number; timeoutMs?: number } = {}): Promise<ConversationDocument> => {
478
+ const interval = opts.intervalMs ?? 3000;
479
+ const deadline = Date.now() + (opts.timeoutMs ?? 180_000);
480
+ for (;;) {
481
+ const d = await this.attachments.get(conversationId, docId);
482
+ if (d.status === "ready" || d.status === "failed" || Date.now() > deadline) return d;
483
+ await sleep(interval);
484
+ }
485
+ },
486
+ };
487
+
488
+ // --- messages + feedback ---
489
+ messages = {
490
+ get: (id: UUID) => this.request<Message>("GET", `/v1/messages/${id}`),
491
+ createFeedback: (messageId: UUID, b: { rating?: number; comment?: string }) =>
492
+ this.request<Feedback>("POST", `/v1/messages/${messageId}/feedback`, { json: b }),
493
+ listFeedback: (messageId: UUID, q?: { limit?: number; cursor?: string }) =>
494
+ this.request<Page<Feedback>>("GET", `/v1/messages/${messageId}/feedback`, { query: q }),
495
+ };
496
+ feedback = {
497
+ get: (id: UUID) => this.request<Feedback>("GET", `/v1/feedback/${id}`),
498
+ delete: (id: UUID) => this.request<void>("DELETE", `/v1/feedback/${id}`),
499
+ };
500
+
501
+ // --- parse jobs ---
502
+ parseJobs = {
503
+ list: (q?: { status_filter?: string; limit?: number; cursor?: string }) => this.request<Page<ParseJob>>("GET", "/v1/parse_jobs", { query: q }),
504
+ get: (id: UUID) => this.request<ParseJob>("GET", `/v1/parse_jobs/${id}`),
505
+ };
506
+
507
+ // --- agents (tenant read-only: choose an agent to query) ---
508
+ agents = {
509
+ list: (q?: { kind?: "query" | "ingestion"; limit?: number; cursor?: string }) => this.request<Page<Agent>>("GET", "/v1/agents", { query: q }),
510
+ listAll: () => this.pageAll<Agent>("/v1/agents"),
511
+ get: (id: UUID) => this.request<Agent>("GET", `/v1/agents/${id}`),
512
+ };
513
+ }
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // Admin client (tenant + key + agent management) — use the ADMIN key
517
+ // ---------------------------------------------------------------------------
518
+ export class AdminClient extends HttpBase {
519
+ tenants = {
520
+ create: (b: Partial<Tenant> & { name: string }) => this.request<Tenant>("POST", "/v1/tenants", { json: b }),
521
+ list: (q?: { limit?: number; cursor?: string }) => this.request<Page<Tenant>>("GET", "/v1/tenants", { query: q }),
522
+ get: (id: UUID) => this.request<Tenant>("GET", `/v1/tenants/${id}`),
523
+ update: (id: UUID, b: Partial<Tenant>) => this.request<Tenant>("PATCH", `/v1/tenants/${id}`, { json: b }),
524
+ delete: (id: UUID, confirmName: string) => this.request<void>("DELETE", `/v1/tenants/${id}`, { query: { confirm: confirmName } }),
525
+ /** Mint a tenant API key — the raw key is in the response ONCE; store it now. */
526
+ createApiKey: (tenantId: UUID, label?: string) => this.request<ApiKeyCreated>("POST", `/v1/tenants/${tenantId}/api-keys`, { json: { label } }),
527
+ listApiKeys: (tenantId: UUID, q?: { limit?: number; cursor?: string }) => this.request<Page<ApiKeyInfo>>("GET", `/v1/tenants/${tenantId}/api-keys`, { query: q }),
528
+ revokeApiKey: (tenantId: UUID, keyId: UUID) => this.request<void>("DELETE", `/v1/tenants/${tenantId}/api-keys/${keyId}`),
529
+ };
530
+
531
+ agents = {
532
+ /** tenant_id explicit, or null for a global/shared agent. */
533
+ create: (b: { name: string; kind: "query" | "ingestion"; tenant_id?: UUID | null; generation_model?: string; top_k?: number; rerank_instruction?: string; max_hops?: number; groundedness_threshold?: number; grounding_enabled?: boolean }) =>
534
+ this.request<Agent>("POST", "/v1/agents", { json: b }),
535
+ update: (id: UUID, b: Partial<Omit<Agent, "id" | "kind" | "tenant_id" | "created_at" | "updated_at">>) =>
536
+ this.request<Agent>("PATCH", `/v1/agents/${id}`, { json: b }),
537
+ delete: (id: UUID) => this.request<void>("DELETE", `/v1/agents/${id}`),
538
+ };
539
+ }
540
+
541
+ // ---------------------------------------------------------------------------
542
+ // helpers
543
+ // ---------------------------------------------------------------------------
544
+ function dispatchSse(frame: string, h: StreamHandlers): void {
545
+ let event = "message";
546
+ const dataLines: string[] = [];
547
+ for (const line of frame.split("\n")) {
548
+ if (line.startsWith("event:")) event = line.slice(6).trim();
549
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
550
+ }
551
+ if (dataLines.length === 0) return;
552
+ const data = safeJson(dataLines.join("\n"));
553
+ h.onEvent?.(event, data);
554
+ switch (event) {
555
+ case "hop": h.onHop?.(data as never); break;
556
+ case "retrieval_contents": h.onSources?.(data as never); break;
557
+ case "token": h.onToken?.((data as { text: string }).text); break;
558
+ case "final": h.onFinal?.(data as QueryResponse); break;
559
+ case "error": h.onError?.(data); break;
560
+ case "done": h.onDone?.(); break;
561
+ }
562
+ }
563
+
564
+ function sleep(ms: number): Promise<void> {
565
+ return new Promise((r) => setTimeout(r, ms));
566
+ }