@autorag/librarian 2.13.8 → 2.13.9

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/core.js CHANGED
@@ -7618,6 +7618,34 @@ var init_memory = __esm(() => {
7618
7618
  });
7619
7619
  });
7620
7620
 
7621
+ // src/minsync/channel-union.ts
7622
+ function unionChannelResults(channels, options = {}) {
7623
+ const byChunk = new Map;
7624
+ const depth = Math.max(0, ...channels.map((channel) => channel.results.length));
7625
+ for (let rank = 0;rank < depth; rank++) {
7626
+ for (const channel of channels) {
7627
+ const result = channel.results[rank];
7628
+ if (result === undefined)
7629
+ continue;
7630
+ const key = `${result.source}\x00${result.id}`;
7631
+ const existing = byChunk.get(key);
7632
+ if (existing === undefined)
7633
+ byChunk.set(key, { result, ranks: { [channel.channel]: rank + 1 } });
7634
+ else
7635
+ existing.ranks[channel.channel] ??= rank + 1;
7636
+ }
7637
+ }
7638
+ const union = [...byChunk.values()].map(({ result, ranks }) => ({
7639
+ ...result,
7640
+ metadata: {
7641
+ ...result.metadata,
7642
+ minsyncChannels: Object.keys(ranks).sort(),
7643
+ minsyncChannelRanks: ranks
7644
+ }
7645
+ }));
7646
+ return options.limit === undefined ? union : union.slice(0, options.limit);
7647
+ }
7648
+
7621
7649
  // src/minsync/local-embedder.ts
7622
7650
  import { spawn as spawn11 } from "node:child_process";
7623
7651
  import { request } from "node:http";
@@ -38721,6 +38749,7 @@ class AutoRAGAgent {
38721
38749
  reranker;
38722
38750
  rerankTopN;
38723
38751
  minSyncMethod;
38752
+ minSyncSparseMethod;
38724
38753
  minSyncEmbedderConfig;
38725
38754
  minSyncBinaryPath;
38726
38755
  jikjiClient;
@@ -38843,6 +38872,7 @@ class AutoRAGAgent {
38843
38872
  ...this.limits.recordCut === undefined ? {} : { recordCut: this.limits.recordCut }
38844
38873
  };
38845
38874
  this.minSyncMethod = new MinSyncVectorMethod(minSyncDefaults);
38875
+ this.minSyncSparseMethod = isSparseOnlyEmbedder(options.minSync?.embedder) ? undefined : new MinSyncVectorMethod({ ...minSyncDefaults, mode: "sparse" });
38846
38876
  this.methodRegistry.register(new MinSyncHybridMethod(minSyncDefaults));
38847
38877
  const registeredDatasourceIds = new Set;
38848
38878
  for (const skill of this.datasourceSkills) {
@@ -40317,13 +40347,7 @@ Warning: the edited config did not validate, so the previous config was restored
40317
40347
  ${list.map((entry, index) => `[${index + 1}] ${entry}`).join(`
40318
40348
  `)}`;
40319
40349
  const perQuery = await Promise.all(searchQueries.map((searchQuery) => Promise.all([
40320
- this.searchProgress.track("minsync", "MinSync semantic search", this.minSyncMethod.retrieve(searchQuery, retrieveOptions), {
40321
- count: (found) => found.length
40322
- }).catch((error) => {
40323
- if (error instanceof MinSyncRequiredError)
40324
- throw error;
40325
- return [];
40326
- }),
40350
+ this.prefetchMinSyncCandidates(searchQuery, retrieveOptions),
40327
40351
  Promise.all(datasources.map((datasourceId) => this.searchProgress.track(`datasource:${datasourceId}`, `Datasource ${datasourceId}`, this.searchSingleDatasourceDocuments(datasourceId, searchQuery, {
40328
40352
  topK: this.limits.prefetch.datasourceTopK,
40329
40353
  scope: options.scope,
@@ -40438,6 +40462,30 @@ ${result.content.replace(/\s+/gu, " ")}`).join(`
40438
40462
 
40439
40463
  `);
40440
40464
  }
40465
+ async prefetchMinSyncCandidates(searchQuery, options) {
40466
+ const sparse = this.reranker === undefined ? undefined : this.minSyncSparseMethod;
40467
+ const run = (id, label, method) => this.searchProgress.track(id, label, method.retrieve(searchQuery, options), { count: (found) => found.length }).catch((error) => {
40468
+ if (error instanceof MinSyncRequiredError)
40469
+ throw error;
40470
+ if (sparse !== undefined) {
40471
+ this.routingDiagnostics.push({
40472
+ code: "retrieval-method-failed",
40473
+ severity: "warning",
40474
+ message: `MinSync ${id === "minsync" ? "dense" : "SPLADE"} search failed and its candidates were skipped; the other channel's were used: ${describeRetrievalError(error)}`,
40475
+ source: id
40476
+ });
40477
+ }
40478
+ return [];
40479
+ });
40480
+ const dense = run("minsync", "MinSync semantic search", this.minSyncMethod);
40481
+ if (sparse === undefined)
40482
+ return dense;
40483
+ const [vector, splade] = await Promise.all([dense, run("minsync-sparse", "MinSync SPLADE search", sparse)]);
40484
+ return unionChannelResults([
40485
+ { channel: "vector", results: vector },
40486
+ { channel: "sparse", results: splade }
40487
+ ]);
40488
+ }
40441
40489
  async prefetchWebSearch(searchQuery, signal) {
40442
40490
  const web = this.webSearchOptions;
40443
40491
  if (web === undefined)
@@ -41815,6 +41863,7 @@ var init_agent = __esm(() => {
41815
41863
  init_merger();
41816
41864
  init_rerank();
41817
41865
  init_scope();
41866
+ init_skip();
41818
41867
  init_search();
41819
41868
  init_model_auth();
41820
41869
  init_answer_citations();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The group runners behind `autorag lite find`: one model-free search per
3
+ * surface — the platform file-name index, local document content, and each
4
+ * connected datasource, each asked on its own so a slow one never blocks the
5
+ * rest.
6
+ */
7
+ import type { RetrievalResult } from "../retrieval/types.js";
8
+ import type { FindGroupDescriptor, FindGroupStatus, FindHit, FindJudge, FindRuntime } from "./types.js";
9
+ /** The result of running one group. */
10
+ export interface FindGroupOutcome {
11
+ readonly status: FindGroupStatus;
12
+ readonly hits: readonly FindHit[];
13
+ readonly error: string | null;
14
+ }
15
+ export interface FindGroupRunner {
16
+ readonly key: string;
17
+ readonly descriptor: FindGroupDescriptor;
18
+ run(): Promise<FindGroupOutcome>;
19
+ }
20
+ export interface BuildGroupRunnersOptions {
21
+ readonly query: string;
22
+ readonly topK: number;
23
+ readonly runtime: FindRuntime;
24
+ readonly judge?: FindJudge;
25
+ readonly platform?: NodeJS.Platform;
26
+ /** Whether the platform file-name index is enabled in config (default true). */
27
+ readonly fileSearchEnabled?: boolean;
28
+ /** Whether local semantic search is enabled in config (default true). */
29
+ readonly semanticEnabled?: boolean;
30
+ /** The enabled, registered datasources, in config order. */
31
+ readonly datasources?: readonly FindGroupDescriptor[];
32
+ }
33
+ /** A retrieval result as a datasource hit; exported for tests. */
34
+ export declare function sourceHit(result: RetrievalResult, query: string): FindHit;
35
+ export declare function buildGroupRunners(options: BuildGroupRunnersOptions): FindGroupRunner[];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `autorag lite find`: a model-free whole search across the platform file-name
3
+ * index, local document content, and every connected datasource. Pure and
4
+ * injectable (runtime, judge); the CLI wires the real ones and prints NDJSON.
5
+ */
6
+ export { type BuildGroupRunnersOptions, buildGroupRunners, type FindGroupOutcome, type FindGroupRunner, sourceHit, } from "./groups.js";
7
+ export { createRelevanceGate, JEV_RELEVANCE_THRESHOLD, type RelevanceCandidate, type RelevanceGateDeps, type SearchRelevanceGate, } from "./relevance.js";
8
+ export { type FindRunOptions, runFind } from "./run.js";
9
+ export { capContent, contentSnippet, escapeSearchRegex, everythingRegexQuery, fsearchRegexQuery, matchesQueryLexically, SEARCH_HIT_CONTENT_LIMIT, } from "./text.js";
10
+ export type { FindDoneLine, FindGroupDescriptor, FindGroupLine, FindGroupStatus, FindGroupsLine, FindHit, FindJudge, FindJudgment, FindKind, FindLine, FindRuntime, FindSearchAnswer, FindVerdict, } from "./types.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The relevance gate for the `content` and `source:*` groups.
3
+ *
4
+ * A deterministic lexical pass auto-keeps obvious hits; everything else goes to
5
+ * the decision model as batched `noul` questions, and only candidates with
6
+ * P(relevant) at or above {@link JEV_RELEVANCE_THRESHOLD} survive, ordered by
7
+ * probability. When no decision model can answer — the judge throws, or returns
8
+ * no numeric verdicts — every candidate is kept unfiltered (fail open).
9
+ */
10
+ import type { FindJudge } from "./types.js";
11
+ /** Minimum judged probability `P(relevant)` required for a non-lexical hit to be shown. */
12
+ export declare const JEV_RELEVANCE_THRESHOLD = 0.5;
13
+ export interface RelevanceCandidate {
14
+ readonly id: string;
15
+ readonly sourceLabel: string;
16
+ readonly title?: string | null;
17
+ readonly subtitle?: string | null;
18
+ readonly content: string;
19
+ }
20
+ export interface SearchRelevanceGate {
21
+ filter<T extends RelevanceCandidate>(query: string, candidates: readonly T[]): Promise<T[]>;
22
+ }
23
+ export interface RelevanceGateDeps {
24
+ readonly judge: FindJudge;
25
+ readonly threshold?: number;
26
+ readonly maxQuestionsPerBatch?: number;
27
+ readonly maxConcurrentBatches?: number;
28
+ }
29
+ export declare function createRelevanceGate(deps: RelevanceGateDeps): SearchRelevanceGate;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Orchestrates a whole search: emit the group list immediately, run every group
3
+ * in parallel, stream one group line per group in completion order, and finish
4
+ * with the done line. One group throwing never affects the others.
5
+ */
6
+ import type { FindGroupDescriptor, FindJudge, FindLine, FindRuntime } from "./types.js";
7
+ export interface FindRunOptions {
8
+ readonly query: string;
9
+ readonly topK: number;
10
+ readonly runtime: FindRuntime;
11
+ readonly judge?: FindJudge;
12
+ readonly platform?: NodeJS.Platform;
13
+ readonly fileSearchEnabled?: boolean;
14
+ readonly semanticEnabled?: boolean;
15
+ /** The enabled, registered datasources, in config order. */
16
+ readonly datasources?: readonly FindGroupDescriptor[];
17
+ /** `--groups` narrowing: run only these keys. */
18
+ readonly groups?: readonly string[];
19
+ /** Emit the `groups` line before running. Pass false when the caller already announced it. */
20
+ readonly announce?: boolean;
21
+ readonly emit: (line: FindLine) => void;
22
+ readonly now?: () => number;
23
+ }
24
+ export declare function runFind(options: FindRunOptions): Promise<void>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Text helpers for the whole-search groups: the relevance gate's deterministic
3
+ * lexical pass, the one-line snippet, and the platform file-name regexes.
4
+ *
5
+ * The lexical pass is ported from the Finder's `matchesQueryLexicallyOrApprox`
6
+ * with the approximate edit-distance step dropped, so it stays dependency-free.
7
+ */
8
+ /** Passed text a hit carries, capped at this many characters. */
9
+ export declare const SEARCH_HIT_CONTENT_LIMIT = 2000;
10
+ /** The passage as one line, cut to {@link SNIPPET_LENGTH} around the first query term that appears in it. */
11
+ export declare function contentSnippet(text: string, query: string): string;
12
+ /** The passage flattened to one line and capped at {@link SEARCH_HIT_CONTENT_LIMIT} characters. */
13
+ export declare function capContent(text: string): string;
14
+ /**
15
+ * Fast deterministic pass-through before Jev: exact query word match (NFKC
16
+ * case-folded, ASCII boundary-aware so `_`/`-`/Hangul neighbors work while
17
+ * partial stems like `art` in `start` do not, plus Korean postposition
18
+ * stripping like `비행기를` → `비행기`). The approximate edit-distance step the
19
+ * Finder had is intentionally dropped: this is dependency-free.
20
+ */
21
+ export declare function matchesQueryLexically(query: string, candidateText: string): boolean;
22
+ /** Escape a literal query so an index's regex mode matches it as a substring. */
23
+ export declare function escapeSearchRegex(text: string): string;
24
+ /**
25
+ * Regex for the fsearch index. macOS stores Hangul file names decomposed (NFD)
26
+ * while typed queries are composed (NFC), and the index compares bytes, so a
27
+ * name matches only when the query uses the same form. Sending both forms as an
28
+ * alternation finds either spelling; a query that has one form (ASCII) stays the
29
+ * plain escaped substring.
30
+ */
31
+ export declare function fsearchRegexQuery(text: string): string;
32
+ /**
33
+ * Everything's `regex:` search function is the working regex surface on the
34
+ * bundled ES build. A bare space ends the function's argument, so spaces become
35
+ * `\x20` to keep a literal substring.
36
+ */
37
+ export declare function everythingRegexQuery(text: string): string;
@@ -0,0 +1,86 @@
1
+ import type { Question } from "jev-use";
2
+ import type { EverythingSearchRequest, EverythingSearchResult } from "../everything/index.js";
3
+ import type { FSearchSearchRequest, FSearchSearchResult } from "../fsearch/index.js";
4
+ import type { RetrievalSelection } from "../retrieval/selection.js";
5
+ import type { RetrievalDiagnostic, RetrievalOptions, RetrievalResult, RetrievalUnsearchedSurface } from "../retrieval/types.js";
6
+ /** What a whole-search hit is: a file name, document content, or a datasource message. */
7
+ export type FindKind = "file" | "content" | "message";
8
+ /** One hit on one group, shared by the filename, content, and datasource surfaces. */
9
+ export interface FindHit {
10
+ readonly id: string;
11
+ readonly kind: FindKind;
12
+ readonly title: string;
13
+ readonly author: string | null;
14
+ readonly snippet: string;
15
+ /** Passed text, capped at {@link SEARCH_HIT_CONTENT_LIMIT} characters. */
16
+ readonly content: string;
17
+ /** Absolute local path for file and content hits; null for datasource messages. */
18
+ readonly path: string | null;
19
+ readonly line: number | null;
20
+ /** Epoch milliseconds, when the source states one. */
21
+ readonly timestamp: number | null;
22
+ readonly url: string | null;
23
+ readonly score: number | null;
24
+ }
25
+ /** The group list line, printed before any group runs. */
26
+ export interface FindGroupDescriptor {
27
+ readonly key: string;
28
+ readonly datasourceId?: string;
29
+ readonly datasourceType?: string;
30
+ }
31
+ /** `off` = the surface is disabled in config; `unavailable` = its index is not built. */
32
+ export type FindGroupStatus = "ready" | "error" | "off" | "unavailable";
33
+ export interface FindGroupsLine {
34
+ readonly type: "groups";
35
+ readonly groups: readonly FindGroupDescriptor[];
36
+ }
37
+ export interface FindGroupLine {
38
+ readonly type: "group";
39
+ readonly key: string;
40
+ readonly status: FindGroupStatus;
41
+ readonly hits: readonly FindHit[];
42
+ /** The group's own failure text verbatim, or null. */
43
+ readonly error: string | null;
44
+ readonly elapsedMs: number;
45
+ }
46
+ export interface FindDoneLine {
47
+ readonly type: "done";
48
+ readonly elapsedMs: number;
49
+ }
50
+ export type FindLine = FindGroupsLine | FindGroupLine | FindDoneLine;
51
+ /** The answer shape `searchSelected` returns; a structural subset of the retrieval engine's. */
52
+ export interface FindSearchAnswer {
53
+ readonly results: readonly RetrievalResult[];
54
+ readonly diagnostics: readonly RetrievalDiagnostic[];
55
+ readonly unsearched: readonly RetrievalUnsearchedSurface[];
56
+ }
57
+ /**
58
+ * The model-free runtime a whole search drives. `AutoRAGLite` satisfies it
59
+ * structurally; tests inject a fake. Only these methods are touched, so a
60
+ * search never enters the model-backed agent loop.
61
+ */
62
+ export interface FindRuntime {
63
+ listDatasources(): readonly {
64
+ readonly datasourceId: string;
65
+ }[];
66
+ searchSelected(query: string, selection?: RetrievalSelection, options?: RetrievalOptions): Promise<FindSearchAnswer>;
67
+ searchFsearch(request: FSearchSearchRequest): Promise<FSearchSearchResult>;
68
+ searchEverything(request: EverythingSearchRequest): Promise<EverythingSearchResult>;
69
+ }
70
+ /**
71
+ * One batched `noul` judgment over relevance questions. The real implementation
72
+ * is `createDecisionModels(...).judge`; a thrown error means "no decision model
73
+ * can answer", and the gate fails open.
74
+ */
75
+ export type FindJudge = (state: string, questions: Question[]) => Promise<FindJudgment>;
76
+ /** The verdicts the gate reads; a structural subset of `jev-use`'s `JudgeResult`. */
77
+ export interface FindJudgment {
78
+ readonly verdicts: readonly FindVerdict[];
79
+ }
80
+ export interface FindVerdict {
81
+ readonly id: string;
82
+ readonly type?: string;
83
+ readonly answer: number | string | null;
84
+ readonly confidence?: number;
85
+ readonly escalate?: boolean;
86
+ }
package/dist/index.js CHANGED
@@ -7618,6 +7618,34 @@ var init_memory = __esm(() => {
7618
7618
  });
7619
7619
  });
7620
7620
 
7621
+ // src/minsync/channel-union.ts
7622
+ function unionChannelResults(channels, options = {}) {
7623
+ const byChunk = new Map;
7624
+ const depth = Math.max(0, ...channels.map((channel) => channel.results.length));
7625
+ for (let rank = 0;rank < depth; rank++) {
7626
+ for (const channel of channels) {
7627
+ const result = channel.results[rank];
7628
+ if (result === undefined)
7629
+ continue;
7630
+ const key = `${result.source}\x00${result.id}`;
7631
+ const existing = byChunk.get(key);
7632
+ if (existing === undefined)
7633
+ byChunk.set(key, { result, ranks: { [channel.channel]: rank + 1 } });
7634
+ else
7635
+ existing.ranks[channel.channel] ??= rank + 1;
7636
+ }
7637
+ }
7638
+ const union = [...byChunk.values()].map(({ result, ranks }) => ({
7639
+ ...result,
7640
+ metadata: {
7641
+ ...result.metadata,
7642
+ minsyncChannels: Object.keys(ranks).sort(),
7643
+ minsyncChannelRanks: ranks
7644
+ }
7645
+ }));
7646
+ return options.limit === undefined ? union : union.slice(0, options.limit);
7647
+ }
7648
+
7621
7649
  // src/minsync/local-embedder.ts
7622
7650
  import { spawn as spawn11 } from "node:child_process";
7623
7651
  import { request } from "node:http";
@@ -38721,6 +38749,7 @@ class AutoRAGAgent {
38721
38749
  reranker;
38722
38750
  rerankTopN;
38723
38751
  minSyncMethod;
38752
+ minSyncSparseMethod;
38724
38753
  minSyncEmbedderConfig;
38725
38754
  minSyncBinaryPath;
38726
38755
  jikjiClient;
@@ -38843,6 +38872,7 @@ class AutoRAGAgent {
38843
38872
  ...this.limits.recordCut === undefined ? {} : { recordCut: this.limits.recordCut }
38844
38873
  };
38845
38874
  this.minSyncMethod = new MinSyncVectorMethod(minSyncDefaults);
38875
+ this.minSyncSparseMethod = isSparseOnlyEmbedder(options.minSync?.embedder) ? undefined : new MinSyncVectorMethod({ ...minSyncDefaults, mode: "sparse" });
38846
38876
  this.methodRegistry.register(new MinSyncHybridMethod(minSyncDefaults));
38847
38877
  const registeredDatasourceIds = new Set;
38848
38878
  for (const skill of this.datasourceSkills) {
@@ -40317,13 +40347,7 @@ Warning: the edited config did not validate, so the previous config was restored
40317
40347
  ${list.map((entry, index) => `[${index + 1}] ${entry}`).join(`
40318
40348
  `)}`;
40319
40349
  const perQuery = await Promise.all(searchQueries.map((searchQuery) => Promise.all([
40320
- this.searchProgress.track("minsync", "MinSync semantic search", this.minSyncMethod.retrieve(searchQuery, retrieveOptions), {
40321
- count: (found) => found.length
40322
- }).catch((error) => {
40323
- if (error instanceof MinSyncRequiredError)
40324
- throw error;
40325
- return [];
40326
- }),
40350
+ this.prefetchMinSyncCandidates(searchQuery, retrieveOptions),
40327
40351
  Promise.all(datasources.map((datasourceId) => this.searchProgress.track(`datasource:${datasourceId}`, `Datasource ${datasourceId}`, this.searchSingleDatasourceDocuments(datasourceId, searchQuery, {
40328
40352
  topK: this.limits.prefetch.datasourceTopK,
40329
40353
  scope: options.scope,
@@ -40438,6 +40462,30 @@ ${result.content.replace(/\s+/gu, " ")}`).join(`
40438
40462
 
40439
40463
  `);
40440
40464
  }
40465
+ async prefetchMinSyncCandidates(searchQuery, options) {
40466
+ const sparse = this.reranker === undefined ? undefined : this.minSyncSparseMethod;
40467
+ const run = (id, label, method) => this.searchProgress.track(id, label, method.retrieve(searchQuery, options), { count: (found) => found.length }).catch((error) => {
40468
+ if (error instanceof MinSyncRequiredError)
40469
+ throw error;
40470
+ if (sparse !== undefined) {
40471
+ this.routingDiagnostics.push({
40472
+ code: "retrieval-method-failed",
40473
+ severity: "warning",
40474
+ message: `MinSync ${id === "minsync" ? "dense" : "SPLADE"} search failed and its candidates were skipped; the other channel's were used: ${describeRetrievalError(error)}`,
40475
+ source: id
40476
+ });
40477
+ }
40478
+ return [];
40479
+ });
40480
+ const dense = run("minsync", "MinSync semantic search", this.minSyncMethod);
40481
+ if (sparse === undefined)
40482
+ return dense;
40483
+ const [vector, splade] = await Promise.all([dense, run("minsync-sparse", "MinSync SPLADE search", sparse)]);
40484
+ return unionChannelResults([
40485
+ { channel: "vector", results: vector },
40486
+ { channel: "sparse", results: splade }
40487
+ ]);
40488
+ }
40441
40489
  async prefetchWebSearch(searchQuery, signal) {
40442
40490
  const web = this.webSearchOptions;
40443
40491
  if (web === undefined)
@@ -41815,6 +41863,7 @@ var init_agent = __esm(() => {
41815
41863
  init_merger();
41816
41864
  init_rerank();
41817
41865
  init_scope();
41866
+ init_skip();
41818
41867
  init_search();
41819
41868
  init_model_auth();
41820
41869
  init_answer_citations();
package/dist/mcp/index.js CHANGED
@@ -7619,6 +7619,34 @@ var init_memory = __esm(() => {
7619
7619
  });
7620
7620
  });
7621
7621
 
7622
+ // src/minsync/channel-union.ts
7623
+ function unionChannelResults(channels, options = {}) {
7624
+ const byChunk = new Map;
7625
+ const depth = Math.max(0, ...channels.map((channel) => channel.results.length));
7626
+ for (let rank = 0;rank < depth; rank++) {
7627
+ for (const channel of channels) {
7628
+ const result = channel.results[rank];
7629
+ if (result === undefined)
7630
+ continue;
7631
+ const key = `${result.source}\x00${result.id}`;
7632
+ const existing = byChunk.get(key);
7633
+ if (existing === undefined)
7634
+ byChunk.set(key, { result, ranks: { [channel.channel]: rank + 1 } });
7635
+ else
7636
+ existing.ranks[channel.channel] ??= rank + 1;
7637
+ }
7638
+ }
7639
+ const union = [...byChunk.values()].map(({ result, ranks }) => ({
7640
+ ...result,
7641
+ metadata: {
7642
+ ...result.metadata,
7643
+ minsyncChannels: Object.keys(ranks).sort(),
7644
+ minsyncChannelRanks: ranks
7645
+ }
7646
+ }));
7647
+ return options.limit === undefined ? union : union.slice(0, options.limit);
7648
+ }
7649
+
7622
7650
  // src/minsync/local-embedder.ts
7623
7651
  import { spawn as spawn11 } from "node:child_process";
7624
7652
  import { request } from "node:http";
@@ -38722,6 +38750,7 @@ class AutoRAGAgent {
38722
38750
  reranker;
38723
38751
  rerankTopN;
38724
38752
  minSyncMethod;
38753
+ minSyncSparseMethod;
38725
38754
  minSyncEmbedderConfig;
38726
38755
  minSyncBinaryPath;
38727
38756
  jikjiClient;
@@ -38844,6 +38873,7 @@ class AutoRAGAgent {
38844
38873
  ...this.limits.recordCut === undefined ? {} : { recordCut: this.limits.recordCut }
38845
38874
  };
38846
38875
  this.minSyncMethod = new MinSyncVectorMethod(minSyncDefaults);
38876
+ this.minSyncSparseMethod = isSparseOnlyEmbedder(options.minSync?.embedder) ? undefined : new MinSyncVectorMethod({ ...minSyncDefaults, mode: "sparse" });
38847
38877
  this.methodRegistry.register(new MinSyncHybridMethod(minSyncDefaults));
38848
38878
  const registeredDatasourceIds = new Set;
38849
38879
  for (const skill of this.datasourceSkills) {
@@ -40318,13 +40348,7 @@ Warning: the edited config did not validate, so the previous config was restored
40318
40348
  ${list.map((entry, index) => `[${index + 1}] ${entry}`).join(`
40319
40349
  `)}`;
40320
40350
  const perQuery = await Promise.all(searchQueries.map((searchQuery) => Promise.all([
40321
- this.searchProgress.track("minsync", "MinSync semantic search", this.minSyncMethod.retrieve(searchQuery, retrieveOptions), {
40322
- count: (found) => found.length
40323
- }).catch((error) => {
40324
- if (error instanceof MinSyncRequiredError)
40325
- throw error;
40326
- return [];
40327
- }),
40351
+ this.prefetchMinSyncCandidates(searchQuery, retrieveOptions),
40328
40352
  Promise.all(datasources.map((datasourceId) => this.searchProgress.track(`datasource:${datasourceId}`, `Datasource ${datasourceId}`, this.searchSingleDatasourceDocuments(datasourceId, searchQuery, {
40329
40353
  topK: this.limits.prefetch.datasourceTopK,
40330
40354
  scope: options.scope,
@@ -40439,6 +40463,30 @@ ${result.content.replace(/\s+/gu, " ")}`).join(`
40439
40463
 
40440
40464
  `);
40441
40465
  }
40466
+ async prefetchMinSyncCandidates(searchQuery, options) {
40467
+ const sparse = this.reranker === undefined ? undefined : this.minSyncSparseMethod;
40468
+ const run = (id, label, method) => this.searchProgress.track(id, label, method.retrieve(searchQuery, options), { count: (found) => found.length }).catch((error) => {
40469
+ if (error instanceof MinSyncRequiredError)
40470
+ throw error;
40471
+ if (sparse !== undefined) {
40472
+ this.routingDiagnostics.push({
40473
+ code: "retrieval-method-failed",
40474
+ severity: "warning",
40475
+ message: `MinSync ${id === "minsync" ? "dense" : "SPLADE"} search failed and its candidates were skipped; the other channel's were used: ${describeRetrievalError(error)}`,
40476
+ source: id
40477
+ });
40478
+ }
40479
+ return [];
40480
+ });
40481
+ const dense = run("minsync", "MinSync semantic search", this.minSyncMethod);
40482
+ if (sparse === undefined)
40483
+ return dense;
40484
+ const [vector, splade] = await Promise.all([dense, run("minsync-sparse", "MinSync SPLADE search", sparse)]);
40485
+ return unionChannelResults([
40486
+ { channel: "vector", results: vector },
40487
+ { channel: "sparse", results: splade }
40488
+ ]);
40489
+ }
40442
40490
  async prefetchWebSearch(searchQuery, signal) {
40443
40491
  const web = this.webSearchOptions;
40444
40492
  if (web === undefined)
@@ -41816,6 +41864,7 @@ var init_agent = __esm(() => {
41816
41864
  init_merger();
41817
41865
  init_rerank();
41818
41866
  init_scope();
41867
+ init_skip();
41819
41868
  init_search();
41820
41869
  init_model_auth();
41821
41870
  init_answer_citations();
@@ -0,0 +1,23 @@
1
+ import type { RetrievalResult } from "../retrieval/types.js";
2
+ /** Ranked results of one MinSync query mode (`vector` or `sparse`), best first. */
3
+ export interface MinSyncChannelResults {
4
+ readonly channel: string;
5
+ readonly results: readonly RetrievalResult[];
6
+ }
7
+ export interface UnionChannelOptions {
8
+ /** Keep at most this many chunks after interleaving. Unset keeps the whole union. */
9
+ readonly limit?: number;
10
+ }
11
+ /**
12
+ * Union of independently ranked MinSync channels, built for a reranker that decides the final order.
13
+ *
14
+ * Channels are interleaved by rank (rank 1 of each channel, then rank 2, ...) so a limit never cuts one
15
+ * channel's head to keep the other's tail, and a chunk found by several channels appears once, at its
16
+ * best interleaved position. Scores are left as the first channel reported them: dense and SPLADE scores
17
+ * are not comparable and nothing here ranks across them. Each chunk records which channels found it and
18
+ * at what 1-based rank (`minsyncChannels`, `minsyncChannelRanks`).
19
+ *
20
+ * Identity is `source` + `id`, the same key the cross-method merger uses, so two chunks of one document
21
+ * stay distinct while the same chunk reached by two channels collapses.
22
+ */
23
+ export declare function unionChannelResults(channels: readonly MinSyncChannelResults[], options?: UnionChannelOptions): RetrievalResult[];
package/dist/profiles.js CHANGED
@@ -22145,6 +22145,34 @@ var init_memory = __esm(() => {
22145
22145
  });
22146
22146
  });
22147
22147
 
22148
+ // src/minsync/channel-union.ts
22149
+ function unionChannelResults(channels, options = {}) {
22150
+ const byChunk = new Map;
22151
+ const depth = Math.max(0, ...channels.map((channel) => channel.results.length));
22152
+ for (let rank = 0;rank < depth; rank++) {
22153
+ for (const channel of channels) {
22154
+ const result = channel.results[rank];
22155
+ if (result === undefined)
22156
+ continue;
22157
+ const key = `${result.source}\x00${result.id}`;
22158
+ const existing = byChunk.get(key);
22159
+ if (existing === undefined)
22160
+ byChunk.set(key, { result, ranks: { [channel.channel]: rank + 1 } });
22161
+ else
22162
+ existing.ranks[channel.channel] ??= rank + 1;
22163
+ }
22164
+ }
22165
+ const union = [...byChunk.values()].map(({ result, ranks }) => ({
22166
+ ...result,
22167
+ metadata: {
22168
+ ...result.metadata,
22169
+ minsyncChannels: Object.keys(ranks).sort(),
22170
+ minsyncChannelRanks: ranks
22171
+ }
22172
+ }));
22173
+ return options.limit === undefined ? union : union.slice(0, options.limit);
22174
+ }
22175
+
22148
22176
  // src/retrieval/chunk-location.ts
22149
22177
  function sectionLabel(line) {
22150
22178
  const text = line.trim();
@@ -38663,6 +38691,7 @@ class AutoRAGAgent {
38663
38691
  reranker;
38664
38692
  rerankTopN;
38665
38693
  minSyncMethod;
38694
+ minSyncSparseMethod;
38666
38695
  minSyncEmbedderConfig;
38667
38696
  minSyncBinaryPath;
38668
38697
  jikjiClient;
@@ -38785,6 +38814,7 @@ class AutoRAGAgent {
38785
38814
  ...this.limits.recordCut === undefined ? {} : { recordCut: this.limits.recordCut }
38786
38815
  };
38787
38816
  this.minSyncMethod = new MinSyncVectorMethod(minSyncDefaults);
38817
+ this.minSyncSparseMethod = isSparseOnlyEmbedder(options.minSync?.embedder) ? undefined : new MinSyncVectorMethod({ ...minSyncDefaults, mode: "sparse" });
38788
38818
  this.methodRegistry.register(new MinSyncHybridMethod(minSyncDefaults));
38789
38819
  const registeredDatasourceIds = new Set;
38790
38820
  for (const skill of this.datasourceSkills) {
@@ -40259,13 +40289,7 @@ Warning: the edited config did not validate, so the previous config was restored
40259
40289
  ${list.map((entry, index) => `[${index + 1}] ${entry}`).join(`
40260
40290
  `)}`;
40261
40291
  const perQuery = await Promise.all(searchQueries.map((searchQuery) => Promise.all([
40262
- this.searchProgress.track("minsync", "MinSync semantic search", this.minSyncMethod.retrieve(searchQuery, retrieveOptions), {
40263
- count: (found) => found.length
40264
- }).catch((error) => {
40265
- if (error instanceof MinSyncRequiredError)
40266
- throw error;
40267
- return [];
40268
- }),
40292
+ this.prefetchMinSyncCandidates(searchQuery, retrieveOptions),
40269
40293
  Promise.all(datasources.map((datasourceId) => this.searchProgress.track(`datasource:${datasourceId}`, `Datasource ${datasourceId}`, this.searchSingleDatasourceDocuments(datasourceId, searchQuery, {
40270
40294
  topK: this.limits.prefetch.datasourceTopK,
40271
40295
  scope: options.scope,
@@ -40380,6 +40404,30 @@ ${result.content.replace(/\s+/gu, " ")}`).join(`
40380
40404
 
40381
40405
  `);
40382
40406
  }
40407
+ async prefetchMinSyncCandidates(searchQuery, options) {
40408
+ const sparse = this.reranker === undefined ? undefined : this.minSyncSparseMethod;
40409
+ const run = (id, label, method) => this.searchProgress.track(id, label, method.retrieve(searchQuery, options), { count: (found) => found.length }).catch((error) => {
40410
+ if (error instanceof MinSyncRequiredError)
40411
+ throw error;
40412
+ if (sparse !== undefined) {
40413
+ this.routingDiagnostics.push({
40414
+ code: "retrieval-method-failed",
40415
+ severity: "warning",
40416
+ message: `MinSync ${id === "minsync" ? "dense" : "SPLADE"} search failed and its candidates were skipped; the other channel's were used: ${describeRetrievalError(error)}`,
40417
+ source: id
40418
+ });
40419
+ }
40420
+ return [];
40421
+ });
40422
+ const dense = run("minsync", "MinSync semantic search", this.minSyncMethod);
40423
+ if (sparse === undefined)
40424
+ return dense;
40425
+ const [vector, splade] = await Promise.all([dense, run("minsync-sparse", "MinSync SPLADE search", sparse)]);
40426
+ return unionChannelResults([
40427
+ { channel: "vector", results: vector },
40428
+ { channel: "sparse", results: splade }
40429
+ ]);
40430
+ }
40383
40431
  async prefetchWebSearch(searchQuery, signal) {
40384
40432
  const web = this.webSearchOptions;
40385
40433
  if (web === undefined)
@@ -41757,6 +41805,7 @@ var init_agent = __esm(() => {
41757
41805
  init_merger();
41758
41806
  init_rerank();
41759
41807
  init_scope();
41808
+ init_skip();
41760
41809
  init_search();
41761
41810
  init_model_auth();
41762
41811
  init_answer_citations();