@dudousxd/nestjs-agent-rag 0.11.0 → 0.13.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
@@ -1,4 +1,4 @@
1
- import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler, RerankOptions } from '@dudousxd/nestjs-agent-core';
1
+ import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler, RerankOptions, ModelProvider, AiToolCtx } from '@dudousxd/nestjs-agent-core';
2
2
 
3
3
  interface ChunkOptions {
4
4
  /** Target max characters per chunk. Default 800. */
@@ -327,7 +327,7 @@ declare function documentIdOf(chunkId: string): string;
327
327
  * both halves in one module would make every retriever ↔ telemetry edge a cycle.
328
328
  */
329
329
  /** Which retrieval strategy answered. `unknown` is a retriever this package did not ship. */
330
- type RetrieverKind = 'embedding' | 'keyword' | 'lexical' | 'hybrid' | 'filtered' | 'reranking' | 'unknown';
330
+ type RetrieverKind = 'embedding' | 'keyword' | 'lexical' | 'hybrid' | 'filtered' | 'reranking' | 'tree-navigation' | 'two-stage' | 'unknown';
331
331
  /** Which backend held the chunks. Omitted entirely for a retriever that holds its own (keyword). */
332
332
  type VectorStoreKind = 'memory' | 'pg' | 'redis';
333
333
  /**
@@ -432,6 +432,34 @@ declare class MemoryVectorStore implements VectorStore {
432
432
  search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
433
433
  }
434
434
 
435
+ /**
436
+ * Turning a natural-language question into full-text search terms, for the lexical leg of
437
+ * `PgLexicalVectorStore`.
438
+ *
439
+ * With the `simple` text-search configuration (no stemming, no stop words; any language) a question
440
+ * searched as is either matches nothing (every word required) or, as "any word", ranks chunks full
441
+ * of "the / of / which / de / que" first. So the query side drops the stop words of the question's
442
+ * language(s) and keeps the meaningful terms, which the store then ranks by how many of them a chunk
443
+ * holds, rare terms weighing more (an IDF over the matching rows, BM25-style).
444
+ */
445
+ /**
446
+ * Postgres' Snowball stop word lists (`tsearch_data/{english,portuguese,spanish}.stop`), minus
447
+ * one-letter words (never terms here) and words that carry meaning in a sibling language
448
+ * ("estado", "sentido").
449
+ */
450
+ declare const DEFAULT_STOP_WORDS: Readonly<Record<string, ReadonlySet<string>>>;
451
+ /**
452
+ * The meaningful terms of a question, in order, deduplicated, at most `max`: the stop words of the
453
+ * language(s) the question is written in are dropped (the language(s) with the most stop words in
454
+ * it, so "sea"/"son" stay terms in an English question although they are Spanish stop words). A
455
+ * question made only of stop words keeps them all (there is nothing else to search for).
456
+ */
457
+ declare function keywordTerms(query: string, max?: number, stopWords?: Readonly<Record<string, ReadonlySet<string>>>): string[];
458
+ /** Whether the user wrote search syntax (a quoted phrase, `-exclusion`) worth honoring as such. */
459
+ declare function hasSearchSyntax(query: string): boolean;
460
+ /** `'a' | 'b' | …` for `to_tsquery` (terms are letter/digit runs, so quoting is enough). */
461
+ declare function anyTermTsquery(terms: string[]): string;
462
+
435
463
  /**
436
464
  * The minimal Postgres surface {@link import('./pg-vector-store.js').PgVectorStore} needs — adapt your
437
465
  * own `pg` / `postgres.js` client to it, so this package pulls in NO driver (bring your own, like the
@@ -502,11 +530,22 @@ interface PgFullTextOptions {
502
530
  */
503
531
  column?: string;
504
532
  /**
505
- * When the question matches nothing as a whole (`websearch_to_tsquery`: every word must appear),
506
- * retry with any word (`w1 | w2 | …`, at most 24 distinct words) so a long natural-language
507
- * question still finds the chunks sharing its rare terms. Default `true`.
533
+ * Search natural-language questions by their meaningful terms (default `true`): the question's
534
+ * stop words are dropped (see {@link PgFullTextOptions.stopWords}) and rows holding any remaining
535
+ * term (at most 24) are ranked by the sum of the matched terms' IDF among those rows (BM25
536
+ * without term frequencies), `ts_rank_cd` breaking ties; a question with explicit search syntax
537
+ * (a quoted phrase, `-word`) is first tried as written. `false`: only `websearch_to_tsquery` on
538
+ * the query as written (every word must appear).
508
539
  */
509
540
  anyTermFallback?: boolean;
541
+ /**
542
+ * Stop word lists dropped from questions, by language; only the list(s) with the most words in
543
+ * the question apply, so a word that is a stop word in another language stays a term. Default
544
+ * {@link DEFAULT_STOP_WORDS} (English, Portuguese, Spanish: Postgres' Snowball lists). `false`
545
+ * keeps every word. With a language `config` (`english`) Postgres drops that language's stop
546
+ * words as well.
547
+ */
548
+ stopWords?: Readonly<Record<string, ReadonlySet<string>>> | false;
510
549
  }
511
550
  interface PgVectorStoreOptions {
512
551
  /** Table name. Default `agent_rag_chunks`. */
@@ -669,9 +708,10 @@ declare class PgVectorStore implements VectorStore {
669
708
  * `LexicalRetriever` and `HybridRetriever` work on Postgres exactly as on RediSearch — the dense leg
670
709
  * over pgvector, the lexical leg over a `tsvector`, both on the same rows and ids.
671
710
  *
672
- * Ranking is `ts_rank_cd` (cover density). The query is parsed with `websearch_to_tsquery` (quoted
673
- * phrases, `-exclusions`, `or`), which requires every word; when that matches nothing it retries
674
- * with any word (see {@link PgFullTextOptions.anyTermFallback}).
711
+ * A question is searched by its meaningful terms: its stop words dropped, rows holding any remaining
712
+ * term ranked by IDF-weighted coverage, `ts_rank_cd` (cover density) breaking ties (see
713
+ * {@link PgFullTextOptions.anyTermFallback}); explicit syntax (quoted phrases, `-exclusions`) goes
714
+ * through `websearch_to_tsquery` first.
675
715
  *
676
716
  * The `tsvector` is either a column you own ({@link PgFullTextOptions.column}) or the expression
677
717
  * `to_tsvector(config, text)`, for which {@link PgVectorStore.schemaStatements} adds a GIN
@@ -682,12 +722,21 @@ declare class PgLexicalVectorStore extends PgVectorStore implements LexicalVecto
682
722
  private readonly tsConfig;
683
723
  private readonly tsColumn;
684
724
  private readonly anyTermFallback;
725
+ private readonly stopWords;
685
726
  constructor(client: PgClientSource, options?: PgVectorStoreOptions & {
686
727
  fullText?: PgFullTextOptions;
687
728
  });
688
729
  schemaStatements(): string[];
689
730
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
731
+ /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */
690
732
  private runTextSearch;
733
+ /**
734
+ * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,
735
+ * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated
736
+ * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full
737
+ * of common words can match most of the table.
738
+ */
739
+ private runRankedTermSearch;
691
740
  /** Must be character-for-character the index expression, or the planner won't use the index. */
692
741
  private tsvector;
693
742
  }
@@ -1399,7 +1448,12 @@ interface OpenAiEmbeddingsOptions {
1399
1448
  apiKey?: string;
1400
1449
  /** Output width for models that support shortening (`text-embedding-3-*`), sent as `dimensions`. */
1401
1450
  dimensions?: number;
1402
- /** Inputs per request. Default 64. */
1451
+ /**
1452
+ * Inputs per request. Default 64. When the server refuses a request for its size (see
1453
+ * {@link isBatchTooLarge}) the batch is split in halves and retried, and later requests use the
1454
+ * size that worked — so a server whose limit is below this (TEI's `--max-client-batch-size`
1455
+ * defaults to 32) costs one refused request, not a failed ingestion.
1456
+ */
1403
1457
  batchSize?: number;
1404
1458
  /**
1405
1459
  * Inputs longer than this many characters are truncated before sending, so one oversized chunk
@@ -1413,15 +1467,28 @@ interface OpenAiEmbeddingsOptions {
1413
1467
  timeoutMs?: number;
1414
1468
  /** Called with the provider-reported token count of every request — for cost accounting. */
1415
1469
  onUsage?: (tokens: number) => void;
1470
+ /**
1471
+ * Called when the server refused a batch for its size and it was split — a misconfiguration worth
1472
+ * surfacing: raise the server's limit (TEI: `--max-client-batch-size`) or lower `batchSize`.
1473
+ */
1474
+ onWarn?: (message: string) => void;
1416
1475
  /** `fetch` to use. Default the global one. */
1417
1476
  fetch?: typeof fetch;
1418
1477
  }
1478
+ /**
1479
+ * Did the server refuse this request for its **size** — too many inputs (TEI's
1480
+ * `--max-client-batch-size`: `batch size 64 > maximum allowed batch size 32`) or too large a
1481
+ * payload? Those are fixed by sending fewer inputs per request; anything else (auth, a bad model,
1482
+ * one input over the context window) is not, and is rethrown as is.
1483
+ */
1484
+ declare function isBatchTooLarge(error: unknown): boolean;
1419
1485
  /**
1420
1486
  * An {@link EmbeddingProvider} over any OpenAI-compatible `POST /v1/embeddings` endpoint, with no SDK
1421
1487
  * dependency: OpenAI itself, or a gateway/local server speaking the same protocol (TEI, Ollama,
1422
1488
  * vLLM, LocalAI…). For a Vercel AI SDK model use `aiSdkEmbedding` from `-ai-sdk` instead.
1423
1489
  *
1424
- * Batches by {@link OpenAiEmbeddingsOptions.batchSize}, restores input order from each item's
1490
+ * Batches by {@link OpenAiEmbeddingsOptions.batchSize} — splitting and retrying a batch the server
1491
+ * refuses as too large, and remembering the size that worked — restores input order from each item's
1425
1492
  * `index`, and fails loudly (an {@link HttpModelError}) on a non-2xx answer or a vector count that
1426
1493
  * doesn't match the inputs. Empty strings are sent as a single space, since OpenAI rejects them.
1427
1494
  *
@@ -1476,4 +1543,788 @@ declare class HttpReranker implements Reranker {
1476
1543
  rerank(query: string, passages: Passage[], options?: RerankOptions): Promise<Passage[]>;
1477
1544
  }
1478
1545
 
1479
- export { type ChunkOptions, type ChunkRecord, type DescribesRetrieval, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, 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 NodePgQueryable, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, 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, openAiEmbeddings, toPgClient };
1546
+ /**
1547
+ * The data model of tree navigation retrieval: a hierarchical table of contents per long document
1548
+ * (PageIndex-style: sections with page ranges and summaries), built once at ingestion and read by an
1549
+ * LLM at query time to walk to the relevant sections instead of ranking hundreds of look-alike chunks.
1550
+ */
1551
+ /**
1552
+ * One addressable piece of a document's text, in reading order — a PDF page, or a section of a
1553
+ * markdown/HTML document. Tree nodes point at ranges of units, and navigation reads the text of the
1554
+ * units a chosen node spans. `index` is the unit's zero-based position; `page` its 1-based page
1555
+ * number when the document has pages.
1556
+ */
1557
+ interface TreeUnit {
1558
+ index: number;
1559
+ text: string;
1560
+ /** 1-based page number (PDF). Defaults to `index + 1` for page input; absent for section input. */
1561
+ page?: number;
1562
+ }
1563
+ /**
1564
+ * A heading the document declares about itself: a PDF outline (bookmark) entry, a markdown header,
1565
+ * a section boundary your extractor already knows. `unit` is the zero-based unit it starts in;
1566
+ * `page` (1-based) is accepted instead for page input. `level` 1 is top level.
1567
+ */
1568
+ interface TreeHeading {
1569
+ title: string;
1570
+ level: number;
1571
+ unit?: number;
1572
+ page?: number;
1573
+ }
1574
+ /** A section as produced by an extractor that already splits on headings. */
1575
+ interface TreeSection {
1576
+ text: string;
1577
+ title?: string;
1578
+ /** Heading depth, 1 = top level. Sections without a level are treated as level 1. */
1579
+ level?: number;
1580
+ /** 1-based page the section starts on, when known. */
1581
+ page?: number;
1582
+ }
1583
+ /**
1584
+ * What {@link import('./build.js').buildDocumentTree} builds from. Give exactly one of `pages`
1585
+ * (one string per page, in order) or `sections` (heading-split text). `outline`, when present, is the
1586
+ * cheapest and most reliable structure there is (a PDF's bookmarks) and is used before any heuristic.
1587
+ */
1588
+ interface DocumentTreeInput {
1589
+ pages?: string[];
1590
+ sections?: TreeSection[];
1591
+ /** Headings the document declares (PDF outline/bookmarks), in reading order. */
1592
+ outline?: TreeHeading[];
1593
+ /** Document title, shown to the navigating LLM and used as the citation source. */
1594
+ title?: string;
1595
+ }
1596
+ /** How the structure of a tree was obtained, cheapest first. */
1597
+ type TreeStructureSource =
1598
+ /** From {@link DocumentTreeInput.outline}. */
1599
+ 'outline'
1600
+ /** From section titles/levels or headings detected in the text — no LLM. */
1601
+ | 'headings'
1602
+ /** An LLM proposed the sections (the document had no usable outline). */
1603
+ | 'llm'
1604
+ /** Fallback: fixed-size page groups. Always succeeds; used when everything above failed or was skipped. */
1605
+ | 'flat';
1606
+ /** Where a node's summary came from. */
1607
+ type TreeSummarySource = 'llm' | 'text' | 'reused';
1608
+ interface DocumentTreeNode {
1609
+ /** Stable within the tree: `0001`, `0002`… in depth-first order (PageIndex's scheme). */
1610
+ id: string;
1611
+ title: string;
1612
+ /** What the section covers — what the navigating LLM reads to decide whether to go in. */
1613
+ summary?: string;
1614
+ summarySource?: TreeSummarySource;
1615
+ /** First and last unit (inclusive, zero-based) the node spans, children included. */
1616
+ unitStart: number;
1617
+ unitEnd: number;
1618
+ /** First and last page (1-based, inclusive) when the document has pages. */
1619
+ pageStart?: number;
1620
+ pageEnd?: number;
1621
+ /**
1622
+ * Titles of descendants removed to respect `maxDepth` — kept because they are routing information:
1623
+ * the pages are still reachable by reading this node, the titles tell the navigator they are here.
1624
+ */
1625
+ keyItems?: string[];
1626
+ /** Hash of the node's title and the text it spans; an incremental rebuild reuses a summary when it matches. */
1627
+ contentHash: string;
1628
+ children: DocumentTreeNode[];
1629
+ }
1630
+ /** What a build cost and how it ended — persisted with the tree, so an operator can see it. */
1631
+ interface TreeBuildStats {
1632
+ llmCalls: number;
1633
+ inputTokens: number;
1634
+ outputTokens: number;
1635
+ durationMs: number;
1636
+ /** Set when the structure step fell back (to `flat`) or summaries were cut short, and why. */
1637
+ fallbackReason?: string;
1638
+ /** True when the per-document budget or timeout ran out at some point during the build. */
1639
+ budgetExhausted: boolean;
1640
+ /** Summaries carried over unchanged from the previous tree (incremental rebuild). */
1641
+ reusedSummaries: number;
1642
+ }
1643
+ interface DocumentTree {
1644
+ documentId: string;
1645
+ /** Format version of this structure. */
1646
+ version: 1;
1647
+ title?: string;
1648
+ /** One-paragraph description of the whole document (LLM, when budget allows). */
1649
+ description?: string;
1650
+ structure: TreeStructureSource;
1651
+ /**
1652
+ * Hash of the input text and every build option that shapes the tree. Equal fingerprint ⇒ the
1653
+ * build would produce the same tree, so it is skipped (see `previous` in the build options).
1654
+ */
1655
+ fingerprint: string;
1656
+ unitCount: number;
1657
+ /** Top-level nodes. */
1658
+ nodes: DocumentTreeNode[];
1659
+ stats: TreeBuildStats;
1660
+ /**
1661
+ * Document metadata used for access filtering, exactly like chunk metadata in a vector store
1662
+ * (tenant, audience, collection…). Stores apply `filter` to it with the vector stores' semantics.
1663
+ */
1664
+ metadata?: Record<string, unknown>;
1665
+ /** Citation-facing origin (title, URL), copied onto returned passages. */
1666
+ source?: string;
1667
+ /** ISO timestamp of the build. */
1668
+ builtAt: string;
1669
+ }
1670
+ /** Walk a tree depth-first (pre-order). */
1671
+ declare function walkTree(nodes: readonly DocumentTreeNode[], path?: readonly DocumentTreeNode[]): Generator<{
1672
+ node: DocumentTreeNode;
1673
+ path: readonly DocumentTreeNode[];
1674
+ }>;
1675
+ /** Index a tree's nodes by id, each with its ancestors. */
1676
+ declare function indexTree(tree: Pick<DocumentTree, 'nodes'>): Map<string, {
1677
+ node: DocumentTreeNode;
1678
+ path: readonly DocumentTreeNode[];
1679
+ }>;
1680
+
1681
+ /**
1682
+ * Deterministic structure detection — the cheap path that runs before any LLM. Finds headings in
1683
+ * page text: markdown headers, and the numbered/keyword headings long structured documents use
1684
+ * (`PART 52`, `Subpart 9.1`, `Item 7.`, `52.236-5 Material and Workmanship.`, `3.2 Scope`).
1685
+ */
1686
+ /** A heading found in (or declared for) the document, before nesting. */
1687
+ interface DetectedHeading {
1688
+ title: string;
1689
+ level: number;
1690
+ unit: number;
1691
+ /** The heading is the first text of its unit, so the previous section ends in the unit before. */
1692
+ atUnitStart: boolean;
1693
+ }
1694
+ /**
1695
+ * Detect headings in each unit's text. `markdownOnly` restricts detection to markdown headers (for
1696
+ * text you know is markdown, where a numbered line is more likely a list item).
1697
+ *
1698
+ * Besides running headers (below), it drops **table-of-contents listings**: five or more
1699
+ * consecutive keyword/numbered heading lines with no body text between them are a contents page
1700
+ * (a regulation part opens with one), not sections — only a leading top-level heading of the run
1701
+ * (`PART 2—…`) and its last line (the first real heading, which body text follows) are kept. Markdown headers are explicit and never dropped this way.
1702
+ */
1703
+ declare function detectHeadings(texts: readonly string[], markdownOnly?: boolean): DetectedHeading[];
1704
+
1705
+ /**
1706
+ * The structured side of every tree-LLM request: what is being asked, as data. The prompt says the
1707
+ * same thing in prose; `task` exists so a deterministic fake (tests, offline dev — see
1708
+ * {@link import('./keyword-tree-llm.js').keywordTreeLlm}) can answer without parsing prose, and so a
1709
+ * host can log or route requests by kind.
1710
+ */
1711
+ type TreeLlmTask = {
1712
+ kind: 'structure';
1713
+ /** The window of pages the LLM is asked to find section starts in. */
1714
+ units: {
1715
+ unit: number;
1716
+ page?: number;
1717
+ text: string;
1718
+ }[];
1719
+ } | {
1720
+ kind: 'summarize';
1721
+ title: string;
1722
+ text: string;
1723
+ } | {
1724
+ kind: 'describe';
1725
+ title?: string;
1726
+ outline: {
1727
+ title: string;
1728
+ summary?: string;
1729
+ }[];
1730
+ } | {
1731
+ kind: 'navigate';
1732
+ query: string;
1733
+ /** `single-pass`: the whole tree is shown; `beam`: one level of candidates at a time. */
1734
+ mode: 'single-pass' | 'beam';
1735
+ candidates: TreeLlmCandidate[];
1736
+ /** Max nodes the answer may name. */
1737
+ maxNodes: number;
1738
+ } | {
1739
+ kind: 'select';
1740
+ query: string;
1741
+ documents: {
1742
+ id: string;
1743
+ title?: string;
1744
+ description?: string;
1745
+ }[];
1746
+ maxDocuments: number;
1747
+ };
1748
+ /** A node as shown to the navigating LLM. */
1749
+ interface TreeLlmCandidate {
1750
+ id: string;
1751
+ title: string;
1752
+ summary?: string;
1753
+ pages?: string;
1754
+ /** Present on beam candidates: whether the node can be expanded. */
1755
+ hasChildren?: boolean;
1756
+ /** Present in single-pass mode: depth (0 = top level). */
1757
+ depth?: number;
1758
+ }
1759
+ interface TreeLlmRequest {
1760
+ task: TreeLlmTask;
1761
+ system: string;
1762
+ prompt: string;
1763
+ /** A cap for the reply. Adapters pass it as `max_tokens` when they can. */
1764
+ maxOutputTokens: number;
1765
+ /** Aborted when the call's timeout or the build/navigation deadline passes. */
1766
+ signal: AbortSignal;
1767
+ }
1768
+ interface TreeLlmResponse {
1769
+ text: string;
1770
+ /** Provider-reported usage. When absent, the budget estimates ~4 characters per token. */
1771
+ usage?: {
1772
+ inputTokens?: number;
1773
+ outputTokens?: number;
1774
+ };
1775
+ }
1776
+ /**
1777
+ * The only LLM surface tree building and navigation need: one prompt in, one text reply out. Replies
1778
+ * are expected to be JSON (for structure / navigate / select) or plain text (summaries); parsing is
1779
+ * lenient (code fences, prose around the object). Bring any model:
1780
+ * {@link treeLlmFromModelProvider} (the agent's own `ModelProvider`), {@link openAiChatTreeLlm}
1781
+ * (any OpenAI-compatible `/chat/completions`), or a function of your own. Use a deterministic
1782
+ * setting (temperature 0) — trees are cached by content, so a flaky model makes rebuilds noisy.
1783
+ */
1784
+ type TreeLlm = (request: TreeLlmRequest) => Promise<TreeLlmResponse>;
1785
+ /**
1786
+ * Hard limits for one document build or one navigation. Every LLM call is checked against them
1787
+ * **before** it is made (with the prompt's estimated size), so a budget is never overshot by more
1788
+ * than one reply, and nothing retries past it: the failure mode this exists for is an indexer that
1789
+ * burned ~1,240 calls on one PDF it could not structure.
1790
+ */
1791
+ interface TreeBudget {
1792
+ /** Max LLM calls. */
1793
+ maxCalls?: number;
1794
+ /** Max input (prompt) tokens across all calls, estimated at ~4 chars/token before each call. */
1795
+ maxInputTokens?: number;
1796
+ /** Max output tokens across all calls. */
1797
+ maxOutputTokens?: number;
1798
+ /** Wall-clock deadline for the whole operation, ms. */
1799
+ timeoutMs?: number;
1800
+ /** Per-call timeout, ms. Default: whatever is left of `timeoutMs`. */
1801
+ callTimeoutMs?: number;
1802
+ }
1803
+ /** Thrown internally when a budget or deadline would be exceeded; callers turn it into a fallback. */
1804
+ declare class TreeBudgetExceededError extends Error {
1805
+ readonly limit: 'calls' | 'inputTokens' | 'outputTokens' | 'timeout';
1806
+ constructor(limit: 'calls' | 'inputTokens' | 'outputTokens' | 'timeout');
1807
+ }
1808
+ /** ~4 characters per token: the budget's estimate when a provider reports no usage. */
1809
+ declare function estimateTokens(text: string): number;
1810
+ /**
1811
+ * Pull the JSON object out of a model reply: bare, fenced in ```json, or surrounded by prose.
1812
+ * Returns `undefined` when there is none (never throws).
1813
+ */
1814
+ declare function parseJsonReply(text: string): Record<string, unknown> | undefined;
1815
+ /**
1816
+ * A {@link TreeLlm} over the agent's own {@link ModelProvider}: one `runTurn` with no tools and a
1817
+ * discarding sink. Usage comes from the provider's report.
1818
+ */
1819
+ declare function treeLlmFromModelProvider(provider: ModelProvider): TreeLlm;
1820
+ interface OpenAiChatTreeLlmOptions {
1821
+ model: string;
1822
+ /** Base URL up to and including `/v1`. Default `https://api.openai.com/v1`. */
1823
+ baseUrl?: string;
1824
+ apiKey?: string;
1825
+ headers?: Record<string, string>;
1826
+ /** Default 0 — trees are cached by content, determinism keeps rebuilds quiet. */
1827
+ temperature?: number;
1828
+ /**
1829
+ * Send `response_format: { type: 'json_object' }` on JSON tasks. Default `false`: not every
1830
+ * OpenAI-compatible server accepts it, and replies are parsed leniently anyway.
1831
+ */
1832
+ jsonMode?: boolean;
1833
+ /** Extra body fields (a reasoning switch, provider routing). */
1834
+ body?: Record<string, unknown>;
1835
+ fetch?: typeof fetch;
1836
+ }
1837
+ /**
1838
+ * A {@link TreeLlm} over any OpenAI-compatible `POST /v1/chat/completions` — OpenAI, a gateway,
1839
+ * vLLM, Ollama, DeepSeek, Qwen… — with no SDK dependency. Throws `HttpModelError` on a non-2xx.
1840
+ */
1841
+ declare function openAiChatTreeLlm(options: OpenAiChatTreeLlmOptions): TreeLlm;
1842
+ /** A get/set cache — a `Map`, an LRU, a Redis wrapper. Values are whole replies. */
1843
+ interface TreeLlmCache {
1844
+ get(key: string): TreeLlmResponse | undefined | Promise<TreeLlmResponse | undefined>;
1845
+ set(key: string, value: TreeLlmResponse): unknown;
1846
+ }
1847
+ /**
1848
+ * Memoize a {@link TreeLlm} by the exact request (task kind, system prompt, prompt, output cap).
1849
+ * With a deterministic model that makes a rebuild of an unchanged section, or a repeated question,
1850
+ * free. Cached replies report zero usage, so they cost nothing against a budget but a call slot.
1851
+ */
1852
+ declare function cachedTreeLlm(llm: TreeLlm, cache?: TreeLlmCache): TreeLlm;
1853
+ /**
1854
+ * A fast, deterministic, non-cryptographic 64-bit hash (two independent 32-bit FNV-1a/murmur-mixed
1855
+ * lanes) as 16 hex chars — content fingerprints and cache keys, not security.
1856
+ */
1857
+ declare function stableHash(text: string): string;
1858
+
1859
+ interface BuildDocumentTreeOptions {
1860
+ documentId: string;
1861
+ /**
1862
+ * The model for summaries, the document description, and structuring documents with no usable
1863
+ * outline. Omit it and the build is LLM-free: outline/headings or page groups, with the start of
1864
+ * each section's text as its summary.
1865
+ */
1866
+ llm?: TreeLlm;
1867
+ /** Deepest level kept (1 = top-level sections only). Deeper headings become `keyItems`. Default 4. */
1868
+ maxDepth?: number;
1869
+ /**
1870
+ * Hard per-document limits for every LLM call of the build. Defaults: 60 calls, 400k input
1871
+ * tokens, 40k output tokens, 5 minutes overall, 60 s per call. Running out is not an error: the
1872
+ * structure step falls back to page groups, and remaining summaries use the section's own text.
1873
+ */
1874
+ budget?: TreeBudget;
1875
+ /** Summarize nodes with the LLM. Default `true` when `llm` is set. */
1876
+ summaries?: boolean;
1877
+ /** A node whose text is shorter than this is its own summary (no LLM call). Default 600 chars. */
1878
+ summaryMinChars?: number;
1879
+ /** Text of a node sent to the summarizer, at most. Default 8 000 chars. */
1880
+ summaryInputChars?: number;
1881
+ /** Concurrent summary calls. Default 4. */
1882
+ concurrency?: number;
1883
+ /** Write a one-paragraph description of the document. Default `true` when `llm` is set. */
1884
+ describe?: boolean;
1885
+ /**
1886
+ * Find headings in the text (markdown headers, `PART 52`, `Item 7.`, `3.2 Scope`…) when there is
1887
+ * no outline and sections carry no titles. `'markdown'` only trusts markdown headers. Default `true`.
1888
+ */
1889
+ detectHeadings?: boolean | 'markdown';
1890
+ /** At least this many headings for a detected structure to be used. Default 3. */
1891
+ minHeadings?: number;
1892
+ /** Ask the LLM for the structure when nothing deterministic worked. Default `true` when `llm` is set. */
1893
+ llmStructure?: boolean;
1894
+ /** Characters of text per structuring call. Default 24 000. */
1895
+ structureWindowChars?: number;
1896
+ /**
1897
+ * Structuring calls allowed. A document needing more windows than this is not sent at all —
1898
+ * it goes straight to page groups instead of spending a budget it cannot finish. Default 12.
1899
+ */
1900
+ maxStructureCalls?: number;
1901
+ /** Units per page-group node (flat trees, and splitting long leaves). Default 5. */
1902
+ groupSize?: number;
1903
+ /** A leaf spanning more units than this is split into page groups. Default 10. */
1904
+ maxLeafUnits?: number;
1905
+ /** Children per level before page groups are grouped again (flat trees). Default 12. */
1906
+ maxChildren?: number;
1907
+ /** The previous tree of this document: an unchanged input returns it as is; changed nodes are re-summarized, the rest reuse their summaries. */
1908
+ previous?: DocumentTree;
1909
+ metadata?: Record<string, unknown>;
1910
+ source?: string;
1911
+ signal?: AbortSignal;
1912
+ }
1913
+ interface BuiltDocumentTree {
1914
+ tree: DocumentTree;
1915
+ /** The units the node ranges index — persist them with the tree ({@link import('./store.js').DocumentTreeStore.put}). */
1916
+ units: TreeUnit[];
1917
+ /** `true` when `previous` had the same fingerprint and was returned without building. */
1918
+ unchanged: boolean;
1919
+ }
1920
+ /** The text a node spans (children included), joined in reading order. */
1921
+ declare function spanText(units: readonly TreeUnit[], start: number, end: number): string;
1922
+ /**
1923
+ * Build a document's navigation tree: a hierarchical table of contents whose nodes carry a title,
1924
+ * the page (or section) range they span, and a summary.
1925
+ *
1926
+ * Structure, cheapest first, never looping:
1927
+ *
1928
+ * 1. the `outline` the input declares (PDF bookmarks);
1929
+ * 2. section titles/levels (`sections` input), or headings detected in the text;
1930
+ * 3. the LLM, window by window (only when a document has no usable outline, and only if every
1931
+ * window fits `maxStructureCalls` and the budget up front);
1932
+ * 4. page groups — always works; the fallback for any failure above.
1933
+ *
1934
+ * Then summaries: top levels first (they matter most for navigation), each node's text sent once,
1935
+ * within the budget; what the budget leaves unsummarized gets the start of its own text. With a
1936
+ * `previous` tree, an unchanged input is returned as is (zero calls), and nodes whose title and text
1937
+ * are unchanged keep their summary.
1938
+ *
1939
+ * The result is deterministic for a deterministic model: node ids are depth-first positions, and
1940
+ * nothing depends on timing except which summaries a timed-out build got to.
1941
+ */
1942
+ declare function buildDocumentTree(input: DocumentTreeInput, options: BuildDocumentTreeOptions): Promise<BuiltDocumentTree>;
1943
+
1944
+ /**
1945
+ * A deterministic stand-in for the tree LLM — no model, no network — for tests and offline
1946
+ * development. It answers from the structured {@link TreeLlmRequest.task}, never the prose:
1947
+ *
1948
+ * - `navigate` / `select`: ranks candidates by how many of the question's terms their title and
1949
+ * summary share, keeps those sharing any (deepest first on ties in single-pass mode); in a beam,
1950
+ * candidates with subsections are expanded, the rest read;
1951
+ * - `summarize`: the section's first sentence; `describe`: the top-level titles;
1952
+ * - `structure`: lines in ALL CAPS (or markdown headers) become level-1 sections.
1953
+ *
1954
+ * Wrap it to count calls, inject failures or slow replies when testing budgets.
1955
+ */
1956
+ declare function keywordTreeLlm(): TreeLlm;
1957
+
1958
+ /** A stored tree without its nodes — what listing and document selection need. */
1959
+ interface DocumentTreeHeader {
1960
+ documentId: string;
1961
+ title?: string;
1962
+ description?: string;
1963
+ structure: DocumentTree['structure'];
1964
+ fingerprint: string;
1965
+ unitCount: number;
1966
+ metadata?: Record<string, unknown>;
1967
+ source?: string;
1968
+ builtAt: string;
1969
+ }
1970
+ /**
1971
+ * Where navigation trees live: one tree per document plus the units (pages/sections) its node
1972
+ * ranges point at, so navigation can read a node's text without going back to the source file.
1973
+ *
1974
+ * `filter` has **exactly** the vector stores' semantics (see `VectorStore`): every key must match
1975
+ * the tree's `metadata`, an array value is match-any, and an empty array denies. It is an access
1976
+ * boundary — a navigator given a tenant filter must not read another tenant's tree — so every read
1977
+ * that takes one applies it. `readUnits` takes none: call it only for a tree a filtered `get` just
1978
+ * returned (which is what the navigator does).
1979
+ */
1980
+ interface DocumentTreeStore {
1981
+ /** Store (replace) a document's tree and units. */
1982
+ put(tree: DocumentTree, units: TreeUnit[]): Promise<void>;
1983
+ /** A document's tree, when it exists **and** passes `filter`. */
1984
+ get(documentId: string, filter?: Record<string, unknown>): Promise<DocumentTree | undefined>;
1985
+ /** The trees of those of `documentIds` that exist and pass `filter` (any order). */
1986
+ getMany(documentIds: readonly string[], filter?: Record<string, unknown>): Promise<DocumentTree[]>;
1987
+ /** Headers (no nodes) of every tree passing `filter`, or of the listed `documentIds` only. */
1988
+ list(options?: {
1989
+ filter?: Record<string, unknown>;
1990
+ documentIds?: readonly string[];
1991
+ limit?: number;
1992
+ }): Promise<DocumentTreeHeader[]>;
1993
+ /** Units `start..end` (inclusive) of a document, in order. */
1994
+ readUnits(documentId: string, start: number, end: number): Promise<TreeUnit[]>;
1995
+ /** Drop a document's tree and units. Unknown ids are a no-op. */
1996
+ remove(documentId: string): Promise<void>;
1997
+ }
1998
+ /** An in-process {@link DocumentTreeStore} — tests, and single-process apps with small corpora. */
1999
+ declare class MemoryDocumentTreeStore implements DocumentTreeStore {
2000
+ private readonly trees;
2001
+ put(tree: DocumentTree, units: TreeUnit[]): Promise<void>;
2002
+ get(documentId: string, filter?: Record<string, unknown>): Promise<DocumentTree | undefined>;
2003
+ getMany(documentIds: readonly string[], filter?: Record<string, unknown>): Promise<DocumentTree[]>;
2004
+ list(options?: {
2005
+ filter?: Record<string, unknown>;
2006
+ documentIds?: readonly string[];
2007
+ limit?: number;
2008
+ }): Promise<DocumentTreeHeader[]>;
2009
+ readUnits(documentId: string, start: number, end: number): Promise<TreeUnit[]>;
2010
+ remove(documentId: string): Promise<void>;
2011
+ }
2012
+
2013
+ interface PgDocumentTreeStoreOptions {
2014
+ /** Trees table. Default `agent_rag_trees`. */
2015
+ table?: string;
2016
+ /** Units (page/section text) table. Default `${table}_units`, i.e. `agent_rag_trees_units`. */
2017
+ unitsTable?: string;
2018
+ /** Unit rows per multi-row `INSERT`. Default 200. */
2019
+ insertBatchSize?: number;
2020
+ }
2021
+ /**
2022
+ * A Postgres {@link DocumentTreeStore}: one row per tree (the header columns, `metadata` as `jsonb`
2023
+ * for filtering, the nodes as `jsonb`) and one row per unit of text. Filters compile to the same SQL
2024
+ * as `PgVectorStore`'s — scalar `@>`, array match-any, empty array denies — so the filter you pass
2025
+ * the vector store works here unchanged.
2026
+ *
2027
+ * Takes the same clients as `PgVectorStore`: a `pg` `Pool`, a Drizzle database, a `postgres.js`
2028
+ * `sql`, or a {@link PgClient}. Call {@link PgDocumentTreeStore.ensureSchema} at boot, or copy
2029
+ * {@link PgDocumentTreeStore.schemaStatements} into a migration. Needs no extension.
2030
+ *
2031
+ * Subclassing: every filtered statement goes through {@link PgDocumentTreeStore.whereConditions}, like
2032
+ * `PgVectorStore.whereConditions`, so a subclass can map a filter onto its own columns.
2033
+ */
2034
+ declare class PgDocumentTreeStore implements DocumentTreeStore {
2035
+ protected readonly client: PgClient;
2036
+ protected readonly table: string;
2037
+ protected readonly unitsTable: string;
2038
+ private readonly insertBatchSize;
2039
+ constructor(client: PgClientSource, options?: PgDocumentTreeStoreOptions);
2040
+ /** The idempotent DDL {@link PgDocumentTreeStore.ensureSchema} runs, in order — for a migration. */
2041
+ schemaStatements(): string[];
2042
+ ensureSchema(): Promise<void>;
2043
+ /**
2044
+ * The SQL conditions a metadata `filter` compiles to (ANDed), appending bindings to `params`.
2045
+ * Override to map a filter onto your own columns. Return `['false']` to deny.
2046
+ */
2047
+ protected whereConditions(filter: Record<string, unknown> | undefined, params: unknown[]): string[];
2048
+ /** Replace the tree and its units in one transaction (when the client has one). */
2049
+ put(tree: DocumentTree, units: TreeUnit[]): Promise<void>;
2050
+ get(documentId: string, filter?: Record<string, unknown>): Promise<DocumentTree | undefined>;
2051
+ getMany(documentIds: readonly string[], filter?: Record<string, unknown>): Promise<DocumentTree[]>;
2052
+ list(options?: {
2053
+ filter?: Record<string, unknown>;
2054
+ documentIds?: readonly string[];
2055
+ limit?: number;
2056
+ }): Promise<DocumentTreeHeader[]>;
2057
+ readUnits(documentId: string, start: number, end: number): Promise<TreeUnit[]>;
2058
+ remove(documentId: string): Promise<void>;
2059
+ }
2060
+
2061
+ interface IndexDocumentTreeOptions extends Omit<BuildDocumentTreeOptions, 'previous'> {
2062
+ store: DocumentTreeStore;
2063
+ /**
2064
+ * Only documents with at least this many units (pages, or sections) get a tree; a shorter one is
2065
+ * skipped and any tree it had is removed. Default 20 — tree navigation pays off on long
2066
+ * structured documents and costs latency and LLM spend for nothing on short notes.
2067
+ */
2068
+ minUnits?: number;
2069
+ /** …or at least this many characters, whichever is met first. Default 60 000 (~15k tokens). */
2070
+ minChars?: number;
2071
+ }
2072
+ type IndexDocumentTreeResult = {
2073
+ status: 'skipped';
2074
+ reason: string;
2075
+ } | {
2076
+ status: 'unchanged';
2077
+ tree: DocumentTree;
2078
+ } | {
2079
+ status: 'built';
2080
+ tree: DocumentTree;
2081
+ };
2082
+ /**
2083
+ * Build (or rebuild) a document's tree and store it — the call to make at upload/ingestion time,
2084
+ * next to chunking and embedding. Short documents are skipped (see `minUnits`/`minChars`); an
2085
+ * unchanged document costs one store read and no LLM call; a changed one reuses the summaries of
2086
+ * its unchanged sections. Never throws for a document the LLM cannot structure: the tree falls back
2087
+ * to page groups and `tree.stats.fallbackReason` says why. Store errors do propagate.
2088
+ */
2089
+ declare function indexDocumentTree(input: DocumentTreeInput, options: IndexDocumentTreeOptions): Promise<IndexDocumentTreeResult>;
2090
+
2091
+ interface TreeNavigationRetrieverOptions {
2092
+ store: DocumentTreeStore;
2093
+ llm: TreeLlm;
2094
+ /**
2095
+ * A tree whose outline (titles, pages, summaries) fits in this many characters is navigated in
2096
+ * **one** call that sees the whole outline — PageIndex's tree search, the variant measured in the
2097
+ * benchmark. Bigger trees are walked level by level (a beam). Default 40 000 (~10k tokens).
2098
+ */
2099
+ singlePassMaxChars?: number;
2100
+ /** Nodes the navigator may expand or read per beam step. Default 4. */
2101
+ beamWidth?: number;
2102
+ /** Beam levels, at most. Nodes still marked "expand" at the last level are read whole. Default 4. */
2103
+ maxSteps?: number;
2104
+ /** Nodes returned per document, at most. Default 5. */
2105
+ maxNodes?: number;
2106
+ /**
2107
+ * Hard per-document limits for one navigation. Defaults: 6 calls, 60k input tokens, 4k output
2108
+ * tokens, 60 s. Running out mid-walk returns what was chosen so far (the nodes last marked for
2109
+ * expansion are read whole) and records `stoppedBy: 'budget'`.
2110
+ */
2111
+ budget?: TreeBudget;
2112
+ /** Characters of text per returned node. Default 12 000. */
2113
+ maxPassageChars?: number;
2114
+ /** Units (pages/sections) read per returned node, at most. Default 25. */
2115
+ maxUnitsPerNode?: number;
2116
+ /** Characters across all passages of one navigation (every document). Default 48 000 (~12k tokens). */
2117
+ maxContextChars?: number;
2118
+ /**
2119
+ * {@link TreeNavigationRetriever.retrieve} with no first stage: documents navigated per query. When
2120
+ * more trees pass the filter, the LLM picks this many from their titles and descriptions
2121
+ * (PageIndex's document selection). Default 3.
2122
+ */
2123
+ maxDocuments?: number;
2124
+ /** Trees shown to document selection, at most. Default 200. */
2125
+ maxSelectionCandidates?: number;
2126
+ /** Documents navigated in parallel. Default 3. */
2127
+ concurrency?: number;
2128
+ }
2129
+ /** One step of a navigation, for the audit trail. */
2130
+ interface NavigationStep {
2131
+ step: number;
2132
+ /** Node ids shown to the LLM this step. */
2133
+ shown: string[];
2134
+ /** Node ids it chose to open further. */
2135
+ expanded: string[];
2136
+ /** Node ids it chose to read. */
2137
+ read: string[];
2138
+ /** The LLM's stated reasoning, verbatim (capped). */
2139
+ reasoning?: string;
2140
+ }
2141
+ /** A node navigation chose, with the path that led to it. */
2142
+ interface NavigatedNode {
2143
+ nodeId: string;
2144
+ title: string;
2145
+ /** Ancestors from the top level down, then the node itself. */
2146
+ path: {
2147
+ id: string;
2148
+ title: string;
2149
+ }[];
2150
+ pageStart?: number;
2151
+ pageEnd?: number;
2152
+ unitStart: number;
2153
+ unitEnd: number;
2154
+ }
2155
+ /** The auditable record of navigating one document. */
2156
+ interface DocumentNavigation {
2157
+ documentId: string;
2158
+ title?: string;
2159
+ mode: 'single-pass' | 'beam';
2160
+ steps: NavigationStep[];
2161
+ nodes: NavigatedNode[];
2162
+ llmCalls: number;
2163
+ inputTokens: number;
2164
+ outputTokens: number;
2165
+ /** Why the walk stopped early, when it did. */
2166
+ stoppedBy?: 'budget' | 'timeout' | 'maxSteps' | 'error' | 'not-found';
2167
+ error?: string;
2168
+ }
2169
+ interface NavigationResult {
2170
+ passages: Passage[];
2171
+ documents: DocumentNavigation[];
2172
+ /** Set when {@link TreeNavigationRetriever.retrieve} had the LLM pick the documents. */
2173
+ selection?: {
2174
+ candidates: number;
2175
+ selected: string[];
2176
+ reasoning?: string;
2177
+ error?: string;
2178
+ };
2179
+ }
2180
+ interface NavigateOptions {
2181
+ /** Access filter applied to every tree read (vector-store semantics). */
2182
+ filter?: Record<string, unknown>;
2183
+ /** Overrides the retriever's per-document budget. */
2184
+ budget?: TreeBudget;
2185
+ signal?: AbortSignal;
2186
+ }
2187
+ /**
2188
+ * Tree navigation retrieval ("vectorless", reasoning-based — PageIndex's method): an LLM reads a
2189
+ * document's table of contents (titles, page ranges, summaries — no body text) and picks the
2190
+ * sections that answer the question; their text comes back as passages, with the path taken and the
2191
+ * model's reasoning recorded so the choice is auditable.
2192
+ *
2193
+ * A small tree is navigated in one call over the whole outline. A large one is walked level by
2194
+ * level: the model sees one level of candidates, opens (`expand`) the promising ones and takes
2195
+ * (`read`) the ones that already answer, until it reaches leaves, runs out of `maxSteps`, or runs
2196
+ * out of its hard per-document {@link TreeBudget} — in which case it returns what it had.
2197
+ *
2198
+ * It pays off on **long structured documents** (regulations, contracts, filings, manuals) where
2199
+ * chunk ranking struggles among hundreds of look-alike passages; on short notes it costs seconds and
2200
+ * LLM spend for nothing. Compose it behind a first stage with {@link TwoStageRetriever}, or give it
2201
+ * to the agent as a tool with {@link createNavigateDocumentTool}.
2202
+ *
2203
+ * As a plain `Retriever`, `retrieve(query, { filter })` navigates the trees that pass `filter`:
2204
+ * all of them when there are at most `maxDocuments`, otherwise the ones the LLM picks from their
2205
+ * descriptions.
2206
+ */
2207
+ declare class TreeNavigationRetriever implements Retriever {
2208
+ private readonly options;
2209
+ readonly store: DocumentTreeStore;
2210
+ private readonly llm;
2211
+ constructor(options: TreeNavigationRetrieverOptions);
2212
+ describeRetrieval(): RetrievalDescriptor;
2213
+ retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
2214
+ /** {@link retrieve} with the audit trail: which documents, which nodes, why. */
2215
+ search(query: string, options?: NavigateOptions): Promise<NavigationResult>;
2216
+ /**
2217
+ * Navigate the given documents (in parallel, up to `concurrency`) and return their passages in
2218
+ * the order given, within `maxContextChars`. Documents without a tree, or whose tree fails
2219
+ * `filter`, yield no passages and `stoppedBy: 'not-found'`.
2220
+ */
2221
+ navigate(query: string, documentIds: readonly string[], options?: NavigateOptions): Promise<NavigationResult>;
2222
+ private walk;
2223
+ private passagesOf;
2224
+ }
2225
+
2226
+ interface TwoStageRetrieverOptions {
2227
+ /** Passages pulled from the first stage. Default 20. */
2228
+ fetchTopK?: number;
2229
+ /**
2230
+ * Only the first this-many distinct documents of the first stage are considered for navigation.
2231
+ * Default 3 — past the top few, a document is more likely a large irrelevant tree (the costliest
2232
+ * failure measured: navigating FAR Part 52 because it ranked fourth) than the answer.
2233
+ */
2234
+ considerDocuments?: number;
2235
+ /** Of those, navigate at most this many (the ones that have a tree). Default 2. */
2236
+ navigateDocuments?: number;
2237
+ /** Per-document navigation budget (overrides the navigator's). */
2238
+ budget?: TreeBudget;
2239
+ /** Keep the first stage's chunks of a navigated document after its tree passages. Default `false`. */
2240
+ keepFirstStageChunks?: boolean;
2241
+ /**
2242
+ * The document a first-stage passage belongs to. Default: `metadata.documentId` when it is a
2243
+ * string, else the chunk id without its `#<n>` suffix (the id scheme `chunkDocuments` produces).
2244
+ */
2245
+ documentIdOf?: (passage: Passage) => string;
2246
+ /** Called with each navigation's audit trail (log it, trace it, show it). */
2247
+ onNavigation?: (navigation: DocumentNavigation) => void;
2248
+ }
2249
+ /**
2250
+ * Hybrid first stage, tree navigation second — the design the benchmark recommends: keep fast
2251
+ * chunk search (hybrid/lexical/dense, 0.3 s, no LLM) for picking **documents**, and navigate the
2252
+ * table of contents only inside the top **long** documents, where chunk ranking is what fails.
2253
+ *
2254
+ * 1. The first stage retrieves `fetchTopK` passages.
2255
+ * 2. Among its first `considerDocuments` distinct documents, those with a tree in the navigator's
2256
+ * store (build trees only for long documents, see `indexDocumentTree`'s `minUnits`) are long;
2257
+ * the first `navigateDocuments` of them are navigated, in parallel, each on its own budget.
2258
+ * 3. The output keeps the first stage's order: a navigated document's section passages take the
2259
+ * place of its chunks (at its best chunk's rank); short documents keep their chunks. A navigation
2260
+ * that fails or finds nothing leaves that document's chunks in place — never worse than stage one.
2261
+ *
2262
+ * `filter` is passed to both stages, so the access boundary holds on either path.
2263
+ */
2264
+ declare class TwoStageRetriever implements Retriever {
2265
+ private readonly firstStage;
2266
+ private readonly navigator;
2267
+ private readonly options;
2268
+ constructor(firstStage: Retriever, navigator: TreeNavigationRetriever, options?: TwoStageRetrieverOptions);
2269
+ describeRetrieval(): RetrievalDescriptor;
2270
+ retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
2271
+ /** {@link retrieve} with the navigations that were run. */
2272
+ search(query: string, options?: RetrieveOptions): Promise<{
2273
+ passages: Passage[];
2274
+ navigations: DocumentNavigation[];
2275
+ }>;
2276
+ }
2277
+
2278
+ interface NavigateDocumentToolOptions {
2279
+ /** Tool name the model sees. Default `navigate_document`. */
2280
+ name?: string;
2281
+ description?: string;
2282
+ /**
2283
+ * The access filter for this call, from the tool context — e.g. `(ctx) => ({ tenant: ctx.actor.tenantRef })`.
2284
+ * Applied to the tree read, with the vector stores' semantics. Omit only for single-tenant corpora.
2285
+ */
2286
+ filter?: (ctx: AiToolCtx) => Record<string, unknown> | undefined;
2287
+ /** Per-call navigation budget (overrides the navigator's). */
2288
+ budget?: TreeBudget;
2289
+ }
2290
+ interface NavigateDocumentInput {
2291
+ documentId: string;
2292
+ question: string;
2293
+ }
2294
+ interface NavigateDocumentOutput {
2295
+ documentId: string;
2296
+ /** `false` when the document has no tree (short documents don't get one) or the caller may not read it. */
2297
+ found: boolean;
2298
+ passages: Passage[];
2299
+ /** The sections chosen and the path to each. */
2300
+ nodes: NavigatedNode[];
2301
+ /** The walk, step by step, with the model's reasoning. */
2302
+ steps: NavigationStep[];
2303
+ stoppedBy?: string;
2304
+ note?: string;
2305
+ }
2306
+ /** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */
2307
+ interface NavigateDocumentTool {
2308
+ spec: ToolSpec;
2309
+ handler: ToolHandler;
2310
+ }
2311
+ /**
2312
+ * The agentic half of two-stage retrieval: `navigate_document(documentId, question)`, a `read`-kind
2313
+ * tool the model calls **after** `search_knowledge` put a long document among its hits — it walks
2314
+ * that document's table of contents to the sections that answer and returns their text, with the
2315
+ * path and reasoning (the citation surface: section titles and page ranges).
2316
+ *
2317
+ * `documentId` may also be a chunk id from a search result (`doc#12`); the chunk suffix is dropped.
2318
+ * A document with no tree answers `found: false` with a note, so the model falls back to the
2319
+ * passages it already has instead of retrying.
2320
+ *
2321
+ * ```ts
2322
+ * providers: [
2323
+ * provideAgentTool(createRetrievalTool(hybrid)),
2324
+ * provideAgentTool(createNavigateDocumentTool(navigator, { filter: (ctx) => ({ tenant: ctx.actor.tenantRef }) })),
2325
+ * ]
2326
+ * ```
2327
+ */
2328
+ declare function createNavigateDocumentTool(navigator: TreeNavigationRetriever, options?: NavigateDocumentToolOptions): NavigateDocumentTool;
2329
+
2330
+ export { type BuildDocumentTreeOptions, type BuiltDocumentTree, type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DetectedHeading, type DocumentNavigation, type DocumentTree, type DocumentTreeHeader, type DocumentTreeInput, type DocumentTreeNode, type DocumentTreeStore, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexDocumentTreeOptions, type IndexDocumentTreeResult, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryDocumentTreeStore, MemoryVectorStore, type MetadataPatch, type NavigateDocumentInput, type NavigateDocumentOutput, type NavigateDocumentTool, type NavigateDocumentToolOptions, type NavigateOptions, type NavigatedNode, type NavigationResult, type NavigationStep, type NodePgQueryable, type OpenAiChatTreeLlmOptions, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, PgDocumentTreeStore, type PgDocumentTreeStoreOptions, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, 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, type TreeBudget, TreeBudgetExceededError, type TreeBuildStats, type TreeHeading, type TreeLlm, type TreeLlmCache, type TreeLlmCandidate, type TreeLlmRequest, type TreeLlmResponse, type TreeLlmTask, TreeNavigationRetriever, type TreeNavigationRetrieverOptions, type TreeSection, type TreeStructureSource, type TreeSummarySource, type TreeUnit, TwoStageRetriever, type TwoStageRetrieverOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyTermTsquery, applyMetadataPatch, buildDocumentTree, cachedTreeLlm, chunkDocuments, chunkIndexOf, chunkText, createNavigateDocumentTool, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, detectHeadings, documentIdOf, emitRagRetrieval, estimateTokens, hasSearchSyntax, indexDocumentTree, indexTree, ingestChunks, ingestDocuments, instrumentRetriever, isBatchTooLarge, isLexicalVectorStore, keywordTerms, keywordTreeLlm, openAiChatTreeLlm, openAiEmbeddings, parseJsonReply, spanText, stableHash, toPgClient, treeLlmFromModelProvider, walkTree };