@graphorin/memory 0.6.0 → 0.7.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/CHANGELOG.md +83 -0
- package/README.md +24 -7
- package/dist/conflict/index.js +0 -1
- package/dist/conflict/pipeline.js +0 -1
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/types.d.ts +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/index.js +2 -0
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +7 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/phases/deep.js +19 -12
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts.map +1 -1
- package/dist/consolidator/phases/induce.js +2 -3
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +35 -8
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +199 -28
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +16 -15
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +129 -47
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +17 -3
- package/dist/consolidator/scheduler.d.ts.map +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/types.d.ts +30 -1
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +10 -0
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.d.ts +1 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
- package/dist/context-engine/compaction/compactor.js +32 -8
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
- package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/marker.js +34 -0
- package/dist/context-engine/compaction/templates/marker.js.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/engine.d.ts +7 -176
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +0 -2
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +8 -49
- package/dist/context-engine/io-types.d.ts +144 -0
- package/dist/context-engine/io-types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +1 -2
- package/dist/context-engine/privacy-filter.d.ts.map +1 -1
- package/dist/context-engine/privacy-filter.js +1 -2
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +43 -52
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +5 -3
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.js +1 -1
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/envelope.js +57 -0
- package/dist/internal/envelope.js.map +1 -0
- package/dist/internal/injection-heuristics.js +0 -0
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/llm-json.js +40 -0
- package/dist/internal/llm-json.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +23 -10
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/memory-interface.d.ts +131 -0
- package/dist/memory-interface.d.ts.map +1 -0
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +2 -3
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts.map +1 -1
- package/dist/search/query-transform.js +2 -3
- package/dist/search/query-transform.js.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -3
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +2 -2
- package/dist/tiers/insight-memory.js +1 -1
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +1 -1
- package/dist/tiers/procedural-memory.js +2 -2
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +58 -4
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +103 -20
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +19 -57
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +53 -214
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +3 -2
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +34 -175
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +5 -4
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +15 -58
- package/dist/tools/runbook-tools.d.ts.map +1 -1
- package/dist/tools/runbook-tools.js.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/package.json +18 -17
- package/src/conflict/index.ts +29 -0
- package/src/conflict/locale-packs/en.ts +142 -0
- package/src/conflict/locale-packs/index.ts +17 -0
- package/src/conflict/locale-packs/types.ts +138 -0
- package/src/conflict/pipeline.ts +371 -0
- package/src/conflict/stages/helpers.ts +65 -0
- package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
- package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
- package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
- package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
- package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
- package/src/conflict/types.ts +263 -0
- package/src/consolidator/budget.ts +312 -0
- package/src/consolidator/decay.ts +246 -0
- package/src/consolidator/dlq.ts +61 -0
- package/src/consolidator/errors.ts +87 -0
- package/src/consolidator/idempotency.ts +27 -0
- package/src/consolidator/index.ts +269 -0
- package/src/consolidator/lock.ts +141 -0
- package/src/consolidator/noise-filter.ts +235 -0
- package/src/consolidator/phases/deep.ts +308 -0
- package/src/consolidator/phases/induce.ts +423 -0
- package/src/consolidator/phases/learned-context.ts +233 -0
- package/src/consolidator/phases/light.ts +166 -0
- package/src/consolidator/phases/reflect.ts +504 -0
- package/src/consolidator/phases/standard.ts +1002 -0
- package/src/consolidator/reconcile.ts +273 -0
- package/src/consolidator/runtime.ts +1049 -0
- package/src/consolidator/scheduler.ts +182 -0
- package/src/consolidator/triggers.ts +134 -0
- package/src/consolidator/types.ts +622 -0
- package/src/context-engine/annotations.ts +161 -0
- package/src/context-engine/auto-recall.ts +92 -0
- package/src/context-engine/compaction/clear-tool-results.ts +232 -0
- package/src/context-engine/compaction/compactor.ts +403 -0
- package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
- package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
- package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
- package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
- package/src/context-engine/compaction/hooks/types.ts +68 -0
- package/src/context-engine/compaction/index.ts +60 -0
- package/src/context-engine/compaction/templates/marker.ts +33 -0
- package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
- package/src/context-engine/compaction/thresholds.ts +107 -0
- package/src/context-engine/compaction/types.ts +255 -0
- package/src/context-engine/engine.ts +970 -0
- package/src/context-engine/index.ts +118 -0
- package/src/context-engine/io-types.ts +162 -0
- package/src/context-engine/locale-packs/en.ts +107 -0
- package/src/context-engine/locale-packs/index.ts +19 -0
- package/src/context-engine/locale-packs/resolver.ts +197 -0
- package/src/context-engine/locale-packs/types.ts +177 -0
- package/src/context-engine/metadata.ts +114 -0
- package/src/context-engine/preambles/inbound-en.ts +19 -0
- package/src/context-engine/privacy-filter.ts +190 -0
- package/src/context-engine/templates/base-en.ts +19 -0
- package/src/context-engine/templates/composer.ts +91 -0
- package/src/context-engine/token-budget.ts +319 -0
- package/src/context-engine/token-counter.ts +157 -0
- package/src/errors/index.ts +273 -0
- package/src/facade.ts +750 -0
- package/src/graph/entity-resolver.ts +364 -0
- package/src/graph/index.ts +24 -0
- package/src/index.ts +259 -0
- package/src/internal/contextualize.ts +173 -0
- package/src/internal/embedder-binding.ts +53 -0
- package/src/internal/envelope.ts +70 -0
- package/src/internal/id.ts +23 -0
- package/src/internal/injection-heuristics.ts +0 -0
- package/src/internal/llm-json.ts +38 -0
- package/src/internal/spans.ts +35 -0
- package/src/internal/storage-adapter.ts +963 -0
- package/src/memory-interface.ts +158 -0
- package/src/migration/embedder-migration.ts +237 -0
- package/src/migration/index.ts +13 -0
- package/src/search/explain.ts +123 -0
- package/src/search/fit-weights.ts +109 -0
- package/src/search/graph-ppr.ts +38 -0
- package/src/search/index.ts +55 -0
- package/src/search/iterative.ts +658 -0
- package/src/search/query-transform.ts +285 -0
- package/src/search/rrf.ts +225 -0
- package/src/search/trust.ts +55 -0
- package/src/search/types.ts +50 -0
- package/src/tiers/episodic-memory.ts +463 -0
- package/src/tiers/index.ts +50 -0
- package/src/tiers/insight-memory.ts +208 -0
- package/src/tiers/procedural-memory.ts +482 -0
- package/src/tiers/semantic-memory.ts +1729 -0
- package/src/tiers/session-memory.ts +271 -0
- package/src/tiers/shared-memory.ts +62 -0
- package/src/tiers/working-memory.ts +368 -0
- package/src/tools/block-tools.ts +172 -0
- package/src/tools/fact-tools.ts +473 -0
- package/src/tools/index.ts +109 -0
- package/src/tools/recall-tools.ts +303 -0
- package/src/tools/runbook-tools.ts +107 -0
- package/src/tools/taint.ts +39 -0
- package/src/tools/types.ts +37 -0
- package/dist/conflict/locale-packs/index.js +0 -4
- package/dist/context-engine/compaction/index.js +0 -10
- package/dist/context-engine/index.d.ts.map +0 -1
- package/dist/context-engine/index.js +0 -25
- package/dist/context-engine/locale-packs/index.js +0 -4
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query transformation for hybrid retrieval (P2-3) - **multi-query /
|
|
3
|
+
* RAG-Fusion** and optional **HyDE**.
|
|
4
|
+
*
|
|
5
|
+
* Auto-recall is a regex trigger and the default search is single-shot:
|
|
6
|
+
* the user's phrasing rarely matches how a memory was stored, so a
|
|
7
|
+
* relevant fact is missed at the retrieval step. This module fans a
|
|
8
|
+
* query into a few reworded variants (one cheap LLM call) and/or writes
|
|
9
|
+
* a hypothetical answer passage to embed (HyDE, arXiv:2212.10496); the
|
|
10
|
+
* caller retrieves each and fuses the lists through the existing RRF
|
|
11
|
+
* reranker. Variant generation is the only thing that touches a
|
|
12
|
+
* provider, so it is **opt-in**: with no transformer configured the
|
|
13
|
+
* default search path stays fully offline and single-shot.
|
|
14
|
+
*
|
|
15
|
+
* The module is provider-agnostic - it imports only `@graphorin/core`
|
|
16
|
+
* types and never performs I/O itself. The concrete `Provider` is
|
|
17
|
+
* injected by the caller via {@link createProviderQueryTransformer},
|
|
18
|
+
* which is resilient: any provider error or unparseable output degrades
|
|
19
|
+
* to "no variants" / "no hypothetical" rather than throwing into the
|
|
20
|
+
* search hot path.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Provider, ProviderRequest } from '@graphorin/core';
|
|
26
|
+
import { stripFence } from '../internal/llm-json.js';
|
|
27
|
+
|
|
28
|
+
/** Default ceiling on reworded variants a provider-backed transformer requests. */
|
|
29
|
+
export const DEFAULT_MAX_QUERY_VARIANTS = 5;
|
|
30
|
+
|
|
31
|
+
/** Default output-token ceiling for a single transform call. */
|
|
32
|
+
const DEFAULT_TRANSFORM_MAX_TOKENS = 256;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pluggable query-transformation seam consumed by
|
|
36
|
+
* `SemanticMemory.search(..., { multiQuery, hyde })`. The built-in
|
|
37
|
+
* provider-backed implementation lives in
|
|
38
|
+
* {@link createProviderQueryTransformer}; advanced callers can supply a
|
|
39
|
+
* bespoke one (e.g. a deterministic synonym expander) to
|
|
40
|
+
* `new SemanticMemory({ queryTransformer })`.
|
|
41
|
+
*
|
|
42
|
+
* Implementations MUST degrade gracefully - return `[]` / `null` rather
|
|
43
|
+
* than throw - so a transform failure never breaks recall.
|
|
44
|
+
*
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
export interface QueryTransformer {
|
|
48
|
+
/**
|
|
49
|
+
* Rewrite a query into up to `count` **additional** reworded variants
|
|
50
|
+
* (the original query is retained by the caller). Returns `[]` when
|
|
51
|
+
* transformation is unavailable or the model returns nothing usable.
|
|
52
|
+
*/
|
|
53
|
+
expand(
|
|
54
|
+
query: string,
|
|
55
|
+
count: number,
|
|
56
|
+
options?: QueryTransformOptions,
|
|
57
|
+
): Promise<ReadonlyArray<string>>;
|
|
58
|
+
/**
|
|
59
|
+
* Generate a single short hypothetical-answer passage to embed (HyDE),
|
|
60
|
+
* or `null` when unavailable. The caller embeds the passage and fuses
|
|
61
|
+
* its vector neighbours into the result.
|
|
62
|
+
*/
|
|
63
|
+
hypothetical(query: string, options?: QueryTransformOptions): Promise<string | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Per-call options for a {@link QueryTransformer}.
|
|
68
|
+
*
|
|
69
|
+
* @stable
|
|
70
|
+
*/
|
|
71
|
+
export interface QueryTransformOptions {
|
|
72
|
+
/** Cancellation signal forwarded to the underlying provider call. */
|
|
73
|
+
readonly signal?: AbortSignal;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* System prompt for multi-query variant generation. Asks for a bare
|
|
78
|
+
* JSON array of standalone rephrasings that preserve the original
|
|
79
|
+
* intent - {@link parseQueryVariants} also tolerates a chatty model.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export const QUERY_EXPANSION_SYSTEM_PROMPT =
|
|
84
|
+
'You rewrite a search query into alternative phrasings to improve recall over a ' +
|
|
85
|
+
'personal memory store. Preserve the original intent; vary vocabulary, specificity, ' +
|
|
86
|
+
'and perspective so different wordings of the same stored memory are matched. ' +
|
|
87
|
+
'Return ONLY a JSON array of strings - each a standalone query, no numbering, no ' +
|
|
88
|
+
'commentary, no duplicates of the original.';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* System prompt for HyDE. Asks for a short, plausible hypothetical
|
|
92
|
+
* answer (not a question rephrase) whose embedding sits near the
|
|
93
|
+
* passage that would actually answer the query.
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export const HYDE_SYSTEM_PROMPT =
|
|
98
|
+
'Write a short, plausible hypothetical answer to the question, phrased as a ' +
|
|
99
|
+
'first-person statement of fact as it might be recorded in a memory store. One to ' +
|
|
100
|
+
'three sentences. Output only the passage - no preamble, no caveats, no markdown.';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the multi-query expansion request. Pure - no I/O. A higher
|
|
104
|
+
* temperature is used deliberately so the variants diverge (the
|
|
105
|
+
* downstream retrieval + RRF fusion stays deterministic).
|
|
106
|
+
*
|
|
107
|
+
* @stable
|
|
108
|
+
*/
|
|
109
|
+
export function buildExpansionRequest(
|
|
110
|
+
query: string,
|
|
111
|
+
count: number,
|
|
112
|
+
options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},
|
|
113
|
+
): ProviderRequest {
|
|
114
|
+
return {
|
|
115
|
+
messages: [
|
|
116
|
+
{
|
|
117
|
+
role: 'user',
|
|
118
|
+
content: `Original query: ${query}\nReturn up to ${count} alternative phrasings as a JSON array of strings.`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
systemMessage: QUERY_EXPANSION_SYSTEM_PROMPT,
|
|
122
|
+
temperature: 0.7,
|
|
123
|
+
maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,
|
|
124
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
125
|
+
outputType: { kind: 'structured' },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the HyDE pseudo-document request. Pure - no I/O.
|
|
131
|
+
*
|
|
132
|
+
* @stable
|
|
133
|
+
*/
|
|
134
|
+
export function buildHydeRequest(
|
|
135
|
+
query: string,
|
|
136
|
+
options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},
|
|
137
|
+
): ProviderRequest {
|
|
138
|
+
return {
|
|
139
|
+
messages: [{ role: 'user', content: query }],
|
|
140
|
+
systemMessage: HYDE_SYSTEM_PROMPT,
|
|
141
|
+
temperature: 0.3,
|
|
142
|
+
maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,
|
|
143
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse the variant-generation model output into a deduped, capped list
|
|
149
|
+
* of reworded queries. Tolerates a JSON array, a `{ "variants": [...] }`
|
|
150
|
+
* / `{ "queries": [...] }` wrapper, fenced blocks, and (as a last
|
|
151
|
+
* resort) a newline / numbered list - so a chatty model never breaks
|
|
152
|
+
* recall. Empty strings and case-insensitive duplicates are dropped;
|
|
153
|
+
* the result is capped at `max`.
|
|
154
|
+
*
|
|
155
|
+
* @stable
|
|
156
|
+
*/
|
|
157
|
+
export function parseQueryVariants(text: string | undefined, max: number): ReadonlyArray<string> {
|
|
158
|
+
if (max <= 0 || text === undefined) return [];
|
|
159
|
+
const candidate = stripFence(text).trim();
|
|
160
|
+
if (candidate.length === 0) return [];
|
|
161
|
+
const raw = extractVariantStrings(candidate);
|
|
162
|
+
const seen = new Set<string>();
|
|
163
|
+
const out: string[] = [];
|
|
164
|
+
for (const item of raw) {
|
|
165
|
+
const trimmed = item.trim();
|
|
166
|
+
if (trimmed.length === 0) continue;
|
|
167
|
+
const key = trimmed.toLowerCase();
|
|
168
|
+
if (seen.has(key)) continue;
|
|
169
|
+
seen.add(key);
|
|
170
|
+
out.push(trimmed);
|
|
171
|
+
if (out.length >= max) break;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse the HyDE model output into a single passage, or `null` when the
|
|
178
|
+
* model returned nothing usable. Strips a fenced block and trims.
|
|
179
|
+
*
|
|
180
|
+
* @stable
|
|
181
|
+
*/
|
|
182
|
+
export function parseHypothetical(text: string | undefined): string | null {
|
|
183
|
+
if (text === undefined) return null;
|
|
184
|
+
const trimmed = stripFence(text).trim();
|
|
185
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wrap a {@link Provider} as a {@link QueryTransformer}. Both methods
|
|
190
|
+
* are **resilient**: a provider error or unparseable response degrades
|
|
191
|
+
* to `[]` / `null` so transformation never throws into `search`. The
|
|
192
|
+
* `maxVariants` ceiling caps the variants requested regardless of the
|
|
193
|
+
* caller's `multiQuery` value (a latency guardrail).
|
|
194
|
+
*
|
|
195
|
+
* @stable
|
|
196
|
+
*/
|
|
197
|
+
export function createProviderQueryTransformer(
|
|
198
|
+
provider: Provider,
|
|
199
|
+
options: { readonly maxVariants?: number; readonly maxTokens?: number } = {},
|
|
200
|
+
): QueryTransformer {
|
|
201
|
+
const maxVariants = Math.max(0, options.maxVariants ?? DEFAULT_MAX_QUERY_VARIANTS);
|
|
202
|
+
const maxTokens = options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS;
|
|
203
|
+
return {
|
|
204
|
+
async expand(query, count, opts = {}): Promise<ReadonlyArray<string>> {
|
|
205
|
+
const want = Math.min(Math.max(0, count), maxVariants);
|
|
206
|
+
if (want === 0 || query.trim().length === 0) return [];
|
|
207
|
+
try {
|
|
208
|
+
const request = buildExpansionRequest(query, want, {
|
|
209
|
+
maxTokens,
|
|
210
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
211
|
+
});
|
|
212
|
+
const response = await provider.generate(request);
|
|
213
|
+
return parseQueryVariants(response.text, want);
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async hypothetical(query, opts = {}): Promise<string | null> {
|
|
219
|
+
if (query.trim().length === 0) return null;
|
|
220
|
+
try {
|
|
221
|
+
const request = buildHydeRequest(query, {
|
|
222
|
+
maxTokens,
|
|
223
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
224
|
+
});
|
|
225
|
+
const response = await provider.generate(request);
|
|
226
|
+
return parseHypothetical(response.text);
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Extract the raw (pre-dedup) variant strings from tolerant model output. */
|
|
235
|
+
function extractVariantStrings(candidate: string): ReadonlyArray<string> {
|
|
236
|
+
const parsed = tryParseJson(candidate);
|
|
237
|
+
if (parsed !== undefined) {
|
|
238
|
+
const arr = Array.isArray(parsed) ? parsed : isRecord(parsed) ? pickArray(parsed) : null;
|
|
239
|
+
if (arr !== null) {
|
|
240
|
+
return arr.filter((v): v is string => typeof v === 'string');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Fallback - split a newline / numbered list and strip list markers.
|
|
244
|
+
return candidate.split('\n').map((line) =>
|
|
245
|
+
line
|
|
246
|
+
.replace(/^\s*(?:[-*•]|\d+[.)])\s*/u, '')
|
|
247
|
+
.replace(/^["']|["']$/gu, '')
|
|
248
|
+
.trim(),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function pickArray(record: Record<string, unknown>): ReadonlyArray<unknown> | null {
|
|
253
|
+
if (Array.isArray(record.variants)) return record.variants;
|
|
254
|
+
if (Array.isArray(record.queries)) return record.queries;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function tryParseJson(text: string): unknown {
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(text);
|
|
261
|
+
} catch {
|
|
262
|
+
const slice = sliceJson(text);
|
|
263
|
+
if (slice === null) return undefined;
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(slice);
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Slice the widest `[...]` array or `{...}` object out of chatty text. */
|
|
273
|
+
function sliceJson(text: string): string | null {
|
|
274
|
+
const arrStart = text.indexOf('[');
|
|
275
|
+
const arrEnd = text.lastIndexOf(']');
|
|
276
|
+
if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);
|
|
277
|
+
const objStart = text.indexOf('{');
|
|
278
|
+
const objEnd = text.lastIndexOf('}');
|
|
279
|
+
if (objStart >= 0 && objEnd > objStart) return text.slice(objStart, objEnd + 1);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
284
|
+
return value !== null && typeof value === 'object';
|
|
285
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { MemoryHit, MemoryRecord } from '@graphorin/core';
|
|
2
|
+
import type { ReRanker, ReRankOptions } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Industry-standard Reciprocal Rank Fusion constant. Lower `k` makes
|
|
6
|
+
* top-ranked items dominate; higher `k` smooths the contribution of
|
|
7
|
+
* lower-ranked items. `k = 60` is the value popularised by Cormack,
|
|
8
|
+
* Clarke, and Büttcher (2009) and is the framework default.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
export const RRF_DEFAULT_K = 60;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Built-in Reciprocal Rank Fusion reranker. Combines multiple ranked
|
|
16
|
+
* lists (vector hits, FTS5 hits, optional entity boost) into a single
|
|
17
|
+
* fused ranking by summing `1 / (k + rank)` per list each item appears
|
|
18
|
+
* in.
|
|
19
|
+
*
|
|
20
|
+
* Properties (verified by the property-based test suite):
|
|
21
|
+
*
|
|
22
|
+
* - **Deterministic.** Identical input lists yield identical output.
|
|
23
|
+
* - **Stable under permutation of input lists.** The fusion is
|
|
24
|
+
* independent of the order in which the input lists are passed in.
|
|
25
|
+
* - **Tie-broken by stable record id.** Records with equal RRF
|
|
26
|
+
* scores keep their first-seen order.
|
|
27
|
+
*
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
export class RRFReranker implements ReRanker {
|
|
31
|
+
readonly id = 'rrf' as const;
|
|
32
|
+
readonly k: number;
|
|
33
|
+
|
|
34
|
+
constructor(k: number = RRF_DEFAULT_K) {
|
|
35
|
+
if (!Number.isFinite(k) || k <= 0) {
|
|
36
|
+
throw new TypeError(
|
|
37
|
+
`[graphorin/memory] RRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
this.k = k;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async rerank<TRecord extends MemoryRecord>(
|
|
44
|
+
query: string,
|
|
45
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,
|
|
46
|
+
options: ReRankOptions = {},
|
|
47
|
+
): Promise<ReadonlyArray<MemoryHit<TRecord>>> {
|
|
48
|
+
void query;
|
|
49
|
+
if (options.signal?.aborted === true) {
|
|
50
|
+
throw new DOMException('RRFReranker aborted', 'AbortError');
|
|
51
|
+
}
|
|
52
|
+
const fused = fuseRrf(lists, this.k, options.labels);
|
|
53
|
+
const topK = options.topK ?? 10;
|
|
54
|
+
return fused.slice(0, Math.max(0, topK));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Weighted-RRF reranker (X-2). Fuses parallel ranked lists through
|
|
60
|
+
* {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who
|
|
61
|
+
* has calibrated list reliability against labels (the P0-1 eval harness)
|
|
62
|
+
* can trust one retriever over another - e.g. up-weight dense vector
|
|
63
|
+
* hits over lexical FTS hits. `weights` is **positional**: aligned to the
|
|
64
|
+
* input `lists` order (the built-in hybrid search passes FTS first, then
|
|
65
|
+
* vector). At equal weights it is identical to {@link RRFReranker}; RRF
|
|
66
|
+
* stays the framework default.
|
|
67
|
+
*
|
|
68
|
+
* Like the RRF reranker it is deterministic and tie-broken by stable
|
|
69
|
+
* record id, and it preserves the upstream `signals` on each hit.
|
|
70
|
+
*
|
|
71
|
+
* @stable
|
|
72
|
+
*/
|
|
73
|
+
export class WeightedRRFReranker implements ReRanker {
|
|
74
|
+
readonly id = 'weighted-rrf' as const;
|
|
75
|
+
readonly k: number;
|
|
76
|
+
readonly weights: ReadonlyArray<number>;
|
|
77
|
+
|
|
78
|
+
constructor(args: { weights: ReadonlyArray<number>; k?: number }) {
|
|
79
|
+
const k = args.k ?? RRF_DEFAULT_K;
|
|
80
|
+
if (!Number.isFinite(k) || k <= 0) {
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
`[graphorin/memory] WeightedRRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
for (const w of args.weights) {
|
|
86
|
+
if (!Number.isFinite(w) || w < 0) {
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
`[graphorin/memory] WeightedRRFReranker: every weight must be a non-negative finite number, got ${String(w)}.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.k = k;
|
|
93
|
+
this.weights = args.weights;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async rerank<TRecord extends MemoryRecord>(
|
|
97
|
+
query: string,
|
|
98
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,
|
|
99
|
+
options: ReRankOptions = {},
|
|
100
|
+
): Promise<ReadonlyArray<MemoryHit<TRecord>>> {
|
|
101
|
+
void query;
|
|
102
|
+
if (options.signal?.aborted === true) {
|
|
103
|
+
throw new DOMException('WeightedRRFReranker aborted', 'AbortError');
|
|
104
|
+
}
|
|
105
|
+
const fused = fuseWeighted(lists, this.weights, this.k, options.labels);
|
|
106
|
+
const topK = options.topK ?? 10;
|
|
107
|
+
return fused.slice(0, Math.max(0, topK));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Pure functional core of the RRF reranker - the equal-weight case of
|
|
113
|
+
* {@link fuseWeighted}. Exported separately so the test suite (and the
|
|
114
|
+
* property-based fuzzer) can exercise the math without the `Promise<…>`
|
|
115
|
+
* wrapping of the public surface.
|
|
116
|
+
*
|
|
117
|
+
* @stable
|
|
118
|
+
*/
|
|
119
|
+
export function fuseRrf<TRecord extends MemoryRecord>(
|
|
120
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,
|
|
121
|
+
k: number,
|
|
122
|
+
labels?: ReadonlyArray<string>,
|
|
123
|
+
): ReadonlyArray<MemoryHit<TRecord>> {
|
|
124
|
+
return fuseWeighted(lists, undefined, k, labels);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Weighted / convex generalization of {@link fuseRrf} (X-2). Each input
|
|
129
|
+
* list `i` contributes `weights[i] · 1 / (k + rank)` to a record's fused
|
|
130
|
+
* score instead of the flat `1 / (k + rank)`, so a caller who has
|
|
131
|
+
* calibrated list reliability against labels (the P0-1 eval harness) can
|
|
132
|
+
* trust one retriever over another. RRF stays the zero-tuning default: a
|
|
133
|
+
* `weights` of `undefined` (or all-`1`) is byte-for-byte identical to
|
|
134
|
+
* {@link fuseRrf}. A missing, non-finite, or negative entry falls back to
|
|
135
|
+
* the neutral weight `1`, so a partial / malformed `weights` never throws
|
|
136
|
+
* or poisons the ranking.
|
|
137
|
+
*
|
|
138
|
+
* Like {@link fuseRrf} the fusion is deterministic, tie-broken by
|
|
139
|
+
* first-seen order, and preserves any upstream `signals` (FTS `bm25`,
|
|
140
|
+
* vector similarity) the hits carried in - the `rrf` / `rrf.list_N`
|
|
141
|
+
* signals it records are the *weighted* contributions, so the fused
|
|
142
|
+
* `score` still equals their sum. Note that - unlike RRF - the result
|
|
143
|
+
* depends on input list *order*, because each weight is bound to a list
|
|
144
|
+
* position.
|
|
145
|
+
*
|
|
146
|
+
* @stable
|
|
147
|
+
*/
|
|
148
|
+
export function fuseWeighted<TRecord extends MemoryRecord>(
|
|
149
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,
|
|
150
|
+
weights: ReadonlyArray<number> | undefined,
|
|
151
|
+
k: number,
|
|
152
|
+
labels?: ReadonlyArray<string>,
|
|
153
|
+
): ReadonlyArray<MemoryHit<TRecord>> {
|
|
154
|
+
type Aggregate = {
|
|
155
|
+
record: TRecord;
|
|
156
|
+
score: number;
|
|
157
|
+
signals: Record<string, number>;
|
|
158
|
+
firstSeenOrder: number;
|
|
159
|
+
};
|
|
160
|
+
const aggregates = new Map<string, Aggregate>();
|
|
161
|
+
let nextOrder = 0;
|
|
162
|
+
for (let listIdx = 0; listIdx < lists.length; listIdx++) {
|
|
163
|
+
const list = lists[listIdx] ?? [];
|
|
164
|
+
const weight = resolveWeight(weights, listIdx);
|
|
165
|
+
// MRET-13: explanation signals key by the caller's retriever-kind
|
|
166
|
+
// label when supplied - `rrf.list_N` is an ephemeral position that
|
|
167
|
+
// changes meaning whenever a leg (vector / HyDE / graph) is
|
|
168
|
+
// conditionally absent from the fan-out.
|
|
169
|
+
const signalKey = `rrf.${labels?.[listIdx] ?? `list_${listIdx}`}`;
|
|
170
|
+
for (let i = 0; i < list.length; i++) {
|
|
171
|
+
const hit = list[i];
|
|
172
|
+
if (hit === undefined) continue;
|
|
173
|
+
const rank = i + 1;
|
|
174
|
+
const contribution = weight / (k + rank);
|
|
175
|
+
const id = hit.record.id;
|
|
176
|
+
const existing = aggregates.get(id);
|
|
177
|
+
if (existing === undefined) {
|
|
178
|
+
const signals: Record<string, number> = {
|
|
179
|
+
...(hit.signals ?? {}),
|
|
180
|
+
rrf: contribution,
|
|
181
|
+
[signalKey]: contribution,
|
|
182
|
+
};
|
|
183
|
+
aggregates.set(id, {
|
|
184
|
+
record: hit.record,
|
|
185
|
+
score: contribution,
|
|
186
|
+
signals,
|
|
187
|
+
firstSeenOrder: nextOrder++,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
existing.score += contribution;
|
|
191
|
+
existing.signals[signalKey] = contribution;
|
|
192
|
+
existing.signals.rrf = (existing.signals.rrf ?? 0) + contribution;
|
|
193
|
+
for (const [key, value] of Object.entries(hit.signals ?? {})) {
|
|
194
|
+
if (key in existing.signals) continue;
|
|
195
|
+
existing.signals[key] = value;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const fused: MemoryHit<TRecord>[] = [];
|
|
201
|
+
for (const a of aggregates.values()) {
|
|
202
|
+
fused.push({
|
|
203
|
+
record: a.record,
|
|
204
|
+
score: a.score,
|
|
205
|
+
signals: Object.freeze({ ...a.signals }),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
fused.sort((a, b) => {
|
|
209
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
210
|
+
const orderA = aggregates.get(a.record.id)?.firstSeenOrder ?? 0;
|
|
211
|
+
const orderB = aggregates.get(b.record.id)?.firstSeenOrder ?? 0;
|
|
212
|
+
return orderA - orderB;
|
|
213
|
+
});
|
|
214
|
+
return fused;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve list `index`'s fusion weight, defaulting a missing / non-finite
|
|
219
|
+
* / negative entry to the neutral `1` (so an absent or malformed
|
|
220
|
+
* `weights` degrades to plain RRF rather than poisoning the ranking).
|
|
221
|
+
*/
|
|
222
|
+
function resolveWeight(weights: ReadonlyArray<number> | undefined, index: number): number {
|
|
223
|
+
const raw = weights?.[index];
|
|
224
|
+
return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : 1;
|
|
225
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C5: trust-aware retrieval ranking (pairs memory-retrieval-01's ranking
|
|
3
|
+
* leg / security-04). The provenance + quarantine discounts that the
|
|
4
|
+
* eviction-time `salience()` composite already applies now also apply at
|
|
5
|
+
* search SCORING time, so a poisoned or foreign-origin memory cannot
|
|
6
|
+
* outrank the user's own words purely on lexical/vector similarity - the
|
|
7
|
+
* direct defense against MINJA-class memory poisoning (query-only
|
|
8
|
+
* injection >95% ASR without rank-time trust).
|
|
9
|
+
*
|
|
10
|
+
* The factor reuses the {@link SalienceWeights} semantics verbatim:
|
|
11
|
+
* quarantined -> `1 - weights.quarantine` (default 0.3x), foreign
|
|
12
|
+
* provenance -> `1 - weights.foreignProvenance` (default 0.8x), first
|
|
13
|
+
* party -> `1` (neutral: default rankings of ordinary user/extraction
|
|
14
|
+
* facts are byte-identical).
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { DEFAULT_SALIENCE_WEIGHTS, type SalienceWeights } from '../consolidator/decay.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `true` for provenance that did not originate first-party (P1-4) -
|
|
23
|
+
* mirrors the eviction-path classification: `null` (legacy / direct
|
|
24
|
+
* write), `'user'`, and `'extraction'` (the consolidator distilling the
|
|
25
|
+
* user's own session) are first-party; `'tool'`, `'imported'`,
|
|
26
|
+
* `'reflection'`, and `'induction'` are foreign.
|
|
27
|
+
*
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
export function isForeignProvenance(provenance: string | null | undefined): boolean {
|
|
31
|
+
return (
|
|
32
|
+
provenance !== null &&
|
|
33
|
+
provenance !== undefined &&
|
|
34
|
+
provenance !== 'user' &&
|
|
35
|
+
provenance !== 'extraction'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Rank-time trust multiplier for one record. `1` for first-party active
|
|
41
|
+
* facts (the common case - zero ranking change), `1 - quarantine` for
|
|
42
|
+
* quarantined-but-included rows, `1 - foreignProvenance` for foreign
|
|
43
|
+
* origin. Quarantine wins when both apply (same branch order as
|
|
44
|
+
* `salience()`).
|
|
45
|
+
*
|
|
46
|
+
* @stable
|
|
47
|
+
*/
|
|
48
|
+
export function trustDiscount(
|
|
49
|
+
record: { readonly status?: string; readonly provenance?: string | null },
|
|
50
|
+
weights: SalienceWeights = DEFAULT_SALIENCE_WEIGHTS,
|
|
51
|
+
): number {
|
|
52
|
+
if (record.status === 'quarantined') return Math.max(0, 1 - weights.quarantine);
|
|
53
|
+
if (isForeignProvenance(record.provenance)) return Math.max(0, 1 - weights.foreignProvenance);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MemoryHit, MemoryRecord } from '@graphorin/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pluggable reranker contract. Concrete implementations live in
|
|
5
|
+
* `@graphorin/memory/search` (the built-in `RRFReranker`) and the
|
|
6
|
+
* Phase 16 optional packages (`@graphorin/reranker-transformersjs`,
|
|
7
|
+
* `@graphorin/reranker-llm`).
|
|
8
|
+
*
|
|
9
|
+
* The reranker accepts one or more parallel ranked lists (vector +
|
|
10
|
+
* FTS5 + optional entity boost) and produces a single fused ranking.
|
|
11
|
+
* Implementations MUST be pure (no I/O outside `signal`-aware
|
|
12
|
+
* dependencies) so the agent runtime can call them mid-stream without
|
|
13
|
+
* deadlocking the main loop.
|
|
14
|
+
*
|
|
15
|
+
* @stable
|
|
16
|
+
*/
|
|
17
|
+
export interface ReRanker {
|
|
18
|
+
/** Stable lowercase identifier surfaced on every span. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/**
|
|
21
|
+
* Rerank one or more parallel ranked lists and return the fused
|
|
22
|
+
* top-K (default `topK = 10`). Each input list must already be
|
|
23
|
+
* sorted by `score` descending.
|
|
24
|
+
*/
|
|
25
|
+
rerank<TRecord extends MemoryRecord>(
|
|
26
|
+
query: string,
|
|
27
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,
|
|
28
|
+
options?: ReRankOptions,
|
|
29
|
+
): Promise<ReadonlyArray<MemoryHit<TRecord>>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-call reranker options. `topK` defaults to `10`; `signal` is
|
|
34
|
+
* propagated to any async work the reranker performs.
|
|
35
|
+
*
|
|
36
|
+
* @stable
|
|
37
|
+
*/
|
|
38
|
+
export interface ReRankOptions {
|
|
39
|
+
readonly topK?: number;
|
|
40
|
+
readonly signal?: AbortSignal;
|
|
41
|
+
/**
|
|
42
|
+
* Stable per-list labels for the explanation signals (MRET-13) -
|
|
43
|
+
* `rrf.<label>` instead of the ephemeral `rrf.list_<index>`. Callers
|
|
44
|
+
* that fan lists out conditionally (multi-query, HyDE, graph) pass
|
|
45
|
+
* retriever-kind labels so X-3 consumers can identify which leg a
|
|
46
|
+
* contribution came from across calls. Missing entries fall back to
|
|
47
|
+
* the positional key.
|
|
48
|
+
*/
|
|
49
|
+
readonly labels?: ReadonlyArray<string>;
|
|
50
|
+
}
|