@dudousxd/nestjs-agent-rag 0.8.0 → 0.10.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
@@ -174,6 +174,32 @@ interface VectorStore {
174
174
  * `size` changed), or {@link VectorStore.remove remove} (orphaned).
175
175
  */
176
176
  listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
177
+ /**
178
+ * Read one document's chunks back — id, text, metadata — ordered by their position in the
179
+ * document, optionally paged with `limit`/`offset`.
180
+ *
181
+ * The gap this closes is that **the text a store holds is otherwise write-only**. Every other way
182
+ * back to it is a ranked query: {@link VectorStore.search} needs an embedding,
183
+ * {@link LexicalVectorStore.searchText} needs a query string, and both return whatever *matched*,
184
+ * in relevance order, capped at `topK`. Neither can answer "show me what was indexed for this
185
+ * document, in order" — the question you ask when retrieval returns the wrong passage and you need
186
+ * to know whether the chunker cut badly or the retriever chose badly. Without this, the only way to
187
+ * see stored text is to reach around the adapter into the backing engine, which is exactly the
188
+ * abstraction break a consumer adopts a store to avoid.
189
+ *
190
+ * Document-scoped rather than filter-scoped, matching {@link VectorStore.remove} and
191
+ * {@link VectorStore.updateMetadata}: ordering is the whole point, and "position within the
192
+ * document" is only meaningful once a document has been picked. Enumerate documents with
193
+ * {@link VectorStore.listDocuments} first when you need to sweep.
194
+ *
195
+ * An unknown `documentId` yields `[]`; it does not throw, matching `remove` and `updateMetadata`.
196
+ *
197
+ * NOT an access-control seam. It takes no metadata `filter` and applies none, so it cannot enforce
198
+ * one — a caller that gates retrieval on a metadata field (tenant, audience, collection) MUST
199
+ * resolve that gate itself before calling, exactly as it would before `remove`. It is deliberately
200
+ * shaped for an operator/debug surface, not for serving end users.
201
+ */
202
+ listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
177
203
  /**
178
204
  * The distinct source document ids currently indexed, optionally narrowed by a metadata `filter` —
179
205
  * {@link VectorStore.listDocuments} without the metadata. Use it when you only need the id set (a
@@ -271,9 +297,90 @@ interface IndexedDocument {
271
297
  id: string;
272
298
  metadata?: Record<string, unknown>;
273
299
  }
300
+ /** One stored chunk, read back verbatim — what {@link VectorStore.listChunks} yields. */
301
+ interface StoredChunk {
302
+ /** The chunk id: `${documentId}#<n>`, or the bare document id for a single-chunk document. */
303
+ id: string;
304
+ /** Zero-based position within the document, parsed from the id. A bare id is chunk `0`. */
305
+ index: number;
306
+ /** The chunk text as it was indexed — the exact string the embedding was computed from. */
307
+ text: string;
308
+ metadata?: Record<string, unknown>;
309
+ }
310
+ /** Paging for {@link VectorStore.listChunks}. Omit both to read the whole document. */
311
+ interface ListChunksOptions {
312
+ /** Max chunks to return, applied after ordering by {@link StoredChunk.index}. */
313
+ limit?: number;
314
+ /** Chunks to skip, applied after ordering. */
315
+ offset?: number;
316
+ }
317
+ /** Chunk `n` of `${documentId}#<n>`; a bare document id (single-chunk document) is `0`. */
318
+ declare function chunkIndexOf(chunkId: string): number;
274
319
  /** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */
275
320
  declare function documentIdOf(chunkId: string): string;
276
321
 
322
+ /**
323
+ * What a retrieval was served BY — the static half of retrieval telemetry, separated from the
324
+ * emitting half (`retrieval-telemetry.ts`) so a store or a retriever can describe itself without
325
+ * importing anything that emits. That separation is not tidiness: `retrieval-telemetry.ts` imports
326
+ * the retrievers' shapes to detect them, and the retrievers import their descriptor kind, so putting
327
+ * both halves in one module would make every retriever ↔ telemetry edge a cycle.
328
+ */
329
+ /** Which retrieval strategy answered. `unknown` is a retriever this package did not ship. */
330
+ type RetrieverKind = 'embedding' | 'keyword' | 'lexical' | 'hybrid' | 'filtered' | 'reranking' | 'unknown';
331
+ /** Which backend held the chunks. Omitted entirely for a retriever that holds its own (keyword). */
332
+ type VectorStoreKind = 'memory' | 'pg' | 'redis';
333
+ /**
334
+ * A retriever's (or a store's) self-description, as it appears on a retrieval event. Every field is
335
+ * optional because the describer is answering only for the layer it *is*: a store knows its backend
336
+ * and namespace but not which strategy queried it; `KeywordRetriever` knows its strategy but has no
337
+ * store at all.
338
+ */
339
+ interface RetrievalDescriptor {
340
+ retriever?: RetrieverKind;
341
+ store?: VectorStoreKind;
342
+ /**
343
+ * The namespace the chunks live in — `PgVectorStore`'s table, `RedisVectorStore`'s RediSearch
344
+ * index. This is the STORE-level namespace, not a per-query metadata scope: a host that partitions
345
+ * one index into logical collections with a metadata filter (the usual multi-tenant shape) reports
346
+ * that through `InstrumentRetrieverOptions.collection`, which overrides this.
347
+ */
348
+ collection?: string;
349
+ }
350
+ /**
351
+ * The optional capability a store or retriever implements to appear on a retrieval event as
352
+ * something other than `unknown`. Deliberately NOT a member of `VectorStore` or of core's
353
+ * `Retriever`: both are implemented outside this package, and adding a required member to either
354
+ * would break every existing implementation on upgrade — for a field that only observability reads.
355
+ */
356
+ interface DescribesRetrieval {
357
+ describeRetrieval(): RetrievalDescriptor;
358
+ }
359
+ /** Structural guard, so a `VectorStore`/`Retriever` is narrowed to the capability without a cast. */
360
+ declare function describesRetrieval(value: unknown): value is DescribesRetrieval;
361
+ /** A candidate's descriptor, or an empty one when it does not carry the capability. */
362
+ declare function describeRetrieval(candidate: unknown): RetrievalDescriptor;
363
+ /**
364
+ * The store half of a wrapped retriever's descriptor: the base's `store`/`collection` with its
365
+ * `retriever` dropped, so the wrapper can stamp its own strategy on top.
366
+ *
367
+ * `new FilteredRetriever(new EmbeddingRetriever(embedder, pg), scope)` therefore reports
368
+ * `{ retriever: 'filtered', store: 'pg', collection: 'agent_rag_chunks' }` rather than losing the
369
+ * store: which backend answered is the thing a "retrievals by store" panel exists to show, and a
370
+ * production wiring is almost always two or three wrappers deep.
371
+ */
372
+ declare function describeSource(base: unknown): Omit<RetrievalDescriptor, 'retriever'>;
373
+ /**
374
+ * The store half shared by EVERY leg of a composed retriever, or nothing when they disagree.
375
+ *
376
+ * A hybrid whose legs are the dense and lexical halves of the same RediSearch index (the shape
377
+ * `LexicalRetriever` exists for) genuinely was served by that one store, and reporting it is what
378
+ * makes the store breakdown add up. A hybrid fusing pgvector with an in-process BM25 index was not
379
+ * served by either alone, and picking the first leg would silently attribute every such retrieval to
380
+ * a store that answered half of it.
381
+ */
382
+ declare function describeSharedSource(legs: readonly unknown[]): Omit<RetrievalDescriptor, 'retriever'>;
383
+
277
384
  /**
278
385
  * An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter
279
386
  * for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).
@@ -286,6 +393,12 @@ declare function documentIdOf(chunkId: string): string;
286
393
  */
287
394
  declare class MemoryVectorStore implements VectorStore {
288
395
  private readonly records;
396
+ /**
397
+ * Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this
398
+ * instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them
399
+ * apart on a dashboard.
400
+ */
401
+ describeRetrieval(): RetrievalDescriptor;
289
402
  upsert(records: VectorRecord[]): Promise<void>;
290
403
  remove(documentId: string): Promise<void>;
291
404
  /**
@@ -296,6 +409,16 @@ declare class MemoryVectorStore implements VectorStore {
296
409
  */
297
410
  updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
298
411
  listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
412
+ /**
413
+ * The chunks of one document, in document order. See {@link VectorStore.listChunks}.
414
+ *
415
+ * Insertion order is not document order — `upsert` stores whatever the caller passed, and a
416
+ * re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index
417
+ * parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the
418
+ * Map. A reference adapter that returned "usually ordered" would let a consumer ship a bug that
419
+ * only shows up on the store tests are NOT written against.
420
+ */
421
+ listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
299
422
  listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
300
423
  removeMany(documentIds: string[]): Promise<void>;
301
424
  /**
@@ -356,6 +479,8 @@ declare class PgVectorStore implements VectorStore {
356
479
  private readonly table;
357
480
  private readonly dimensions;
358
481
  constructor(client: PgClient, options?: PgVectorStoreOptions);
482
+ /** Telemetry self-description — the table is the namespace a retrieval was served from. */
483
+ describeRetrieval(): RetrievalDescriptor;
359
484
  /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
360
485
  ensureSchema(): Promise<void>;
361
486
  upsert(records: VectorRecord[]): Promise<void>;
@@ -374,6 +499,19 @@ declare class PgVectorStore implements VectorStore {
374
499
  */
375
500
  updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
376
501
  listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
502
+ /**
503
+ * The chunks of one document, in document order. See {@link VectorStore.listChunks}.
504
+ *
505
+ * The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain
506
+ * `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the
507
+ * trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which
508
+ * carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two
509
+ * adapters.
510
+ *
511
+ * `embedding` is left out of the projection: it is the widest column in the table and useless to a
512
+ * caller reading text back.
513
+ */
514
+ listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
377
515
  /** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */
378
516
  listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
379
517
  /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
@@ -463,6 +601,12 @@ declare class RedisVectorStore implements LexicalVectorStore {
463
601
  private readonly filterableFields;
464
602
  private readonly lexicalScorer;
465
603
  constructor(client: RedisSearchClient, options?: RedisVectorStoreOptions);
604
+ /**
605
+ * Telemetry self-description — the RediSearch INDEX, not the key prefix: the index is what both
606
+ * legs query, and two stores sharing one index (the dense and lexical halves of a hybrid) must
607
+ * report the same namespace or `describeSharedSource` would see them disagree and drop the store.
608
+ */
609
+ describeRetrieval(): RetrievalDescriptor;
466
610
  /**
467
611
  * Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
468
612
  * and if it DOES exist, compare what's there against this store's configuration instead of
@@ -550,6 +694,25 @@ declare class RedisVectorStore implements LexicalVectorStore {
550
694
  * document owns. The glob is escaped so `gone` does not reach into `goner#0`.
551
695
  */
552
696
  private chunkKeys;
697
+ /**
698
+ * The chunks of one document, in document order. See {@link VectorStore.listChunks}.
699
+ *
700
+ * Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.
701
+ * Two reasons, and the second is the one that matters: the query language has no way to ask for
702
+ * "every chunk of this document" — `id` is the key name, not an indexed field — and `FT.SEARCH`
703
+ * caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does
704
+ * not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and
705
+ * {@link RedisVectorStore.updateMetadata} already use, so "the chunks this returns" and "the chunks
706
+ * a delete would take" cannot drift apart.
707
+ *
708
+ * It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this
709
+ * is an inspection path (one document, on demand) and not something retrieval runs — but it is why
710
+ * this must not be put on a hot path or called in a loop over a corpus.
711
+ *
712
+ * `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing
713
+ * a caller reading text back can do with it.
714
+ */
715
+ listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
553
716
  listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
554
717
  /**
555
718
  * The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:
@@ -653,6 +816,8 @@ declare class EmbeddingRetriever implements Retriever {
653
816
  private readonly store;
654
817
  private readonly options;
655
818
  constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
819
+ /** Telemetry self-description — the dense strategy, over whatever store it was handed. */
820
+ describeRetrieval(): RetrievalDescriptor;
656
821
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
657
822
  }
658
823
 
@@ -712,6 +877,12 @@ declare class KeywordRetriever implements Retriever {
712
877
  private readonly k1;
713
878
  private readonly b;
714
879
  constructor(options?: KeywordRetrieverOptions);
880
+ /**
881
+ * Telemetry self-description. No `store`: this retriever holds its own copy of the corpus in
882
+ * process, so attributing its retrievals to whichever vector store happens to hold the same chunks
883
+ * would credit a backend that was never queried.
884
+ */
885
+ describeRetrieval(): RetrievalDescriptor;
715
886
  /** How many chunks are currently indexed. */
716
887
  get size(): number;
717
888
  /** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */
@@ -757,6 +928,8 @@ declare class KeywordRetriever implements Retriever {
757
928
  declare class LexicalRetriever implements Retriever {
758
929
  private readonly store;
759
930
  constructor(store: LexicalVectorStore);
931
+ /** Telemetry self-description — the store's own lexical index, so the store is the same one. */
932
+ describeRetrieval(): RetrievalDescriptor;
760
933
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
761
934
  }
762
935
 
@@ -808,6 +981,8 @@ declare class HybridRetriever implements Retriever {
808
981
  private readonly retrievers;
809
982
  private readonly options;
810
983
  constructor(retrievers: Retriever[], options?: HybridRetrieverOptions);
984
+ /** Telemetry self-description — the store only when every leg agrees; see `describeSharedSource`. */
985
+ describeRetrieval(): RetrievalDescriptor;
811
986
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
812
987
  }
813
988
 
@@ -826,6 +1001,8 @@ declare class RerankingRetriever implements Retriever {
826
1001
  private readonly reranker;
827
1002
  private readonly options;
828
1003
  constructor(base: Retriever, reranker: Reranker, options?: RerankingRetrieverOptions);
1004
+ /** Telemetry self-description — the two-stage strategy, over the base's store. */
1005
+ describeRetrieval(): RetrievalDescriptor;
829
1006
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
830
1007
  }
831
1008
 
@@ -842,15 +1019,178 @@ declare class FilteredRetriever implements Retriever {
842
1019
  private readonly base;
843
1020
  private readonly filter;
844
1021
  constructor(base: Retriever, filter: Record<string, unknown>);
1022
+ /** Telemetry self-description — this scope, over the base's store. */
1023
+ describeRetrieval(): RetrievalDescriptor;
845
1024
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
846
1025
  }
847
1026
 
1027
+ /**
1028
+ * Retrieval telemetry: one diagnostics event per retrieval, carrying how long it took, how much came
1029
+ * back, how good it was, and what served it.
1030
+ *
1031
+ * Until this existed, retrieval reached Telescope only as the *tool call* that wrapped it (see
1032
+ * `retrieval-tool.ts`), which says a retrieval happened and nothing else. The questions an operator
1033
+ * actually arrives with — is retrieval slow, is it returning nothing, is one collection colder than
1034
+ * the rest, did the scores collapse after a model swap — are all invisible in a tool-call row,
1035
+ * because a tool call that returns zero passages and one that returns five perfect ones are the same
1036
+ * row with the same `ok` status.
1037
+ *
1038
+ * ## One seam, not six
1039
+ *
1040
+ * Instrumentation is a WRAPPER over the core `Retriever` SPI ({@link instrumentRetriever}), not code
1041
+ * inside each retriever. Every retriever in this package — and every retriever outside it — is the
1042
+ * same one-method interface, so one wrapper covers all of them, including ones this package has
1043
+ * never heard of. The alternative, an `emit` inside each of the six `retrieve` methods, would have
1044
+ * had to be repeated in each, skipped by third-party retrievers entirely, and would have double- and
1045
+ * triple-counted the composed ones (a hybrid over two legs emitting three events for one retrieval).
1046
+ *
1047
+ * ## How a composed retrieval reports
1048
+ *
1049
+ * **The outermost instrumented retriever in a call tree emits; anything instrumented inside it stays
1050
+ * silent.** A retrieval is one question the caller asked, so it is one event no matter how many
1051
+ * retrievers cooperated to answer it: `RerankingRetriever` over a `HybridRetriever` over a dense and
1052
+ * a lexical leg is ONE retrieval, reported as `retriever: 'reranking'` with the duration of the
1053
+ * whole thing and the scores the caller actually got back.
1054
+ *
1055
+ * The suppression is enforced with an {@link AsyncLocalStorage} flag rather than by asking hosts to
1056
+ * wrap carefully, because the wrapping that double-counts is the natural one: a host that wraps its
1057
+ * top-level retriever and *also* wraps the dense leg it shares with another code path has not made a
1058
+ * mistake, and would otherwise get two events per retrieval, a zero-hit rate computed over a mixture
1059
+ * of legs and composites, and a retrieval count roughly twice its query count.
1060
+ *
1061
+ * ## Opt-out safety, and what the hot path pays
1062
+ *
1063
+ * The event goes out on `aviary:rag:retrieval` via `@dudousxd/nestjs-diagnostics`, whose `emit` is
1064
+ * gated on `channel.hasSubscribers` and never throws. This wrapper checks that same gate BEFORE it
1065
+ * does anything at all — no clock read, no `AsyncLocalStorage.run`, no score arithmetic — and
1066
+ * delegates straight through when nothing is listening. A host that wires this package without
1067
+ * Telescope (or any subscriber) pays two `Map` lookups and a boolean per retrieval, and nothing else
1068
+ * changes: the wrapper returns the base retriever's passages verbatim, and rethrows its errors
1069
+ * unchanged.
1070
+ *
1071
+ * ## Why this channel is not merged into the typed `ChannelRegistry`
1072
+ *
1073
+ * `@dudousxd/nestjs-agent-rag-media` already emits `media.ingested`/`media.removed`/`media.skipped`/
1074
+ * `media.failed` on the same `rag` lib, and flip's host app emits `rag:search` on it. Declaring
1075
+ * `ChannelRegistry['rag'] = { retrieval: RagRetrievalEvent }` here would narrow `EventOf<'rag'>` to
1076
+ * this one key process-wide and turn every one of those sibling emits into a compile error in any
1077
+ * project that installs both packages. The payload stays typed where it matters — {@link emitRagRetrieval}
1078
+ * takes {@link RagRetrievalEvent} — and the wire contract is documented below instead.
1079
+ */
1080
+ /** The diagnostics lib segment. Shared with `@dudousxd/nestjs-agent-rag-media`'s `media.*` events. */
1081
+ declare const RAG_DIAGNOSTIC_LIB = "rag";
1082
+ /** The diagnostics event segment. */
1083
+ declare const RAG_RETRIEVAL_EVENT = "retrieval";
1084
+ /**
1085
+ * The full channel name a subscriber listens on: `aviary:rag:retrieval`. This is the wire contract —
1086
+ * `@dudousxd/nestjs-agent-telescope` subscribes to this exact string rather than importing this
1087
+ * package, which is what keeps the telescope extension free of a dependency on the retrieval stack
1088
+ * (and lets a host that uses neither still observe retrieval with a bare `diagnostics_channel`
1089
+ * subscription).
1090
+ */
1091
+ declare const RAG_RETRIEVAL_CHANNEL: string;
1092
+ /**
1093
+ * The payload of one retrieval event. Counts, durations, kinds and scores only — never the query
1094
+ * text and never passage text. Both are user content, and this lands in an ops store with a short
1095
+ * retention window and a Slack-facing redaction budget; everything here is safe to put on a
1096
+ * dashboard and is what tells the failure modes apart anyway.
1097
+ *
1098
+ * The DURATION is not in here: it rides the envelope (`emit`'s `opts.durationMs`), which is the field
1099
+ * Telescope's OTel exporter turns into a latency histogram. Putting it in the payload instead would
1100
+ * have made retrieval latency a number somebody reads out of a JSON blob rather than a graph.
1101
+ */
1102
+ interface RagRetrievalEvent {
1103
+ /** Which strategy answered — the OUTERMOST one, for a composed retriever. */
1104
+ retriever: RetrieverKind;
1105
+ /** Which backend held the chunks. Absent for an in-process retriever, or when legs disagree. */
1106
+ store?: VectorStoreKind;
1107
+ /** Store namespace (pg table / RediSearch index), or the host's logical collection. */
1108
+ collection?: string;
1109
+ /** What the caller asked for, so `chunks < topK` is readable as "the corpus ran out". */
1110
+ topK: number;
1111
+ /** How many passages came back. */
1112
+ chunks: number;
1113
+ /**
1114
+ * `chunks === 0`. Carried as its own flag rather than left to be derived, because the zero-hit RATE
1115
+ * is the headline number and deriving it means every consumer re-deriving the same predicate.
1116
+ */
1117
+ zeroHit: boolean;
1118
+ /** Best score in the result, rounded to 4dp. Absent on a zero-hit. */
1119
+ topScore?: number;
1120
+ /** Mean score across the result, rounded to 4dp. Absent on a zero-hit. */
1121
+ meanScore?: number;
1122
+ /**
1123
+ * The retrieval threw. Reported rather than dropped because a failed retrieval and an empty one
1124
+ * look identical from the agent's side — both become "I couldn't find anything" — so a channel
1125
+ * that only carried successes would go quiet exactly when retrieval broke.
1126
+ */
1127
+ failed: boolean;
1128
+ /** The error message, when `failed`. */
1129
+ error?: string;
1130
+ }
1131
+ /** Publish one retrieval event. `durationMs` rides the envelope; see {@link RagRetrievalEvent}. */
1132
+ declare function emitRagRetrieval(event: RagRetrievalEvent, durationMs: number): void;
1133
+ interface InstrumentRetrieverOptions {
1134
+ /**
1135
+ * Override the auto-detected {@link RetrievalDescriptor}, field by field. Needed for a retriever or
1136
+ * store from outside this package (which reports `retriever: 'unknown'` and no store), and for a
1137
+ * host that wants its own naming.
1138
+ */
1139
+ describe?: RetrievalDescriptor;
1140
+ /**
1141
+ * The logical collection this retrieval searched, when the store namespace is not it. Pass a
1142
+ * function to read it off the call's metadata filter — the multi-tenant shape, where one index
1143
+ * holds every collection and the filter picks one:
1144
+ *
1145
+ * ```ts
1146
+ * instrumentRetriever(retriever, {
1147
+ * collection: (filter) => (typeof filter?.collectionId === 'string' ? filter.collectionId : undefined),
1148
+ * })
1149
+ * ```
1150
+ *
1151
+ * Returning `undefined` falls back to the store namespace, which is the right answer for a query
1152
+ * scoped to many collections at once: a breakdown panel needs ONE label per retrieval, and
1153
+ * inventing a synthetic `"a+b+c"` bucket would produce a chart whose slices are combinations
1154
+ * rather than collections.
1155
+ */
1156
+ collection?: string | ((filter: Record<string, unknown> | undefined) => string | undefined);
1157
+ }
1158
+ /**
1159
+ * Wrap a retriever so every `retrieve` publishes a {@link RagRetrievalEvent} on
1160
+ * {@link RAG_RETRIEVAL_CHANNEL}. Wrap the retriever you actually hand to the agent — the outermost
1161
+ * one — and nothing else needs changing:
1162
+ *
1163
+ * ```ts
1164
+ * const retriever = instrumentRetriever(
1165
+ * new HybridRetriever([new EmbeddingRetriever(embedder, store), new LexicalRetriever(store)]),
1166
+ * );
1167
+ * ```
1168
+ *
1169
+ * {@link createRetrievalTool} already does this for the tool it builds, so agentic retrieval is
1170
+ * instrumented with no host change at all; call this directly for inject-mode retrieval
1171
+ * (`AgentModule.forRoot({ retrieval })`) or for a host that calls `retrieve` itself. Wrapping twice
1172
+ * is harmless — see the composed-retrieval section above.
1173
+ */
1174
+ declare function instrumentRetriever(retriever: Retriever, options?: InstrumentRetrieverOptions): Retriever;
1175
+
848
1176
  interface RetrievalToolOptions {
849
1177
  /** Tool name the model sees. Default `search_knowledge`. */
850
1178
  name?: string;
851
1179
  description?: string;
852
1180
  /** Passages per search. Default lets the retriever decide (typically 5). */
853
1181
  topK?: number;
1182
+ /**
1183
+ * Retrieval telemetry (`aviary:rag:retrieval` — duration, chunk count, score distribution, store
1184
+ * and collection). **On by default**, because this tool is the once-per-retrieval seam in agentic
1185
+ * mode: instrumenting here is what makes the RAG panels work for a host that only ever calls
1186
+ * `createRetrievalTool`, with no wiring at all. It costs nothing when nobody subscribes (see
1187
+ * {@link instrumentRetriever}), and cannot double-count when the host has already wrapped the
1188
+ * retriever itself.
1189
+ *
1190
+ * Pass `false` to opt a single tool out — a host that reports retrieval its own way, or one whose
1191
+ * corpus is small enough that the events are noise.
1192
+ */
1193
+ telemetry?: boolean | InstrumentRetrieverOptions;
854
1194
  }
855
1195
  /** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */
856
1196
  interface RetrievalTool {
@@ -862,10 +1202,14 @@ interface RetrievalTool {
862
1202
  * decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources
863
1203
  * flow to the frontend and telescope through the normal tool-call machinery (the citation surface).
864
1204
  *
1205
+ * The retrieval ITSELF is also instrumented (`aviary:rag:retrieval`) unless `telemetry: false` — the
1206
+ * tool-call row says a retrieval happened, the retrieval event says how long it took, how much came
1207
+ * back and how good it was. See {@link instrumentRetriever}.
1208
+ *
865
1209
  * ```ts
866
1210
  * providers: [provideAgentTool(createRetrievalTool(retriever))]
867
1211
  * ```
868
1212
  */
869
1213
  declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
870
1214
 
871
- export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
1215
+ export { type ChunkOptions, type ChunkRecord, type DescribesRetrieval, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagRetrievalEvent, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, documentIdOf, emitRagRetrieval, ingestChunks, ingestDocuments, instrumentRetriever, isLexicalVectorStore };