@graphorin/memory 0.5.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/conflict/index.d.ts +3 -0
- package/dist/conflict/index.js +7 -0
- package/dist/conflict/locale-packs/en.js +262 -0
- package/dist/conflict/locale-packs/en.js.map +1 -0
- package/dist/conflict/locale-packs/index.js +4 -0
- package/dist/conflict/locale-packs/types.d.ts +103 -0
- package/dist/conflict/locale-packs/types.d.ts.map +1 -0
- package/dist/conflict/locale-packs/types.js +55 -0
- package/dist/conflict/locale-packs/types.js.map +1 -0
- package/dist/conflict/pipeline.d.ts +35 -0
- package/dist/conflict/pipeline.d.ts.map +1 -0
- package/dist/conflict/pipeline.js +257 -0
- package/dist/conflict/pipeline.js.map +1 -0
- package/dist/conflict/stages/helpers.js +66 -0
- package/dist/conflict/stages/helpers.js.map +1 -0
- package/dist/conflict/stages/stage1-exact-dedup.js +21 -0
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -0
- package/dist/conflict/stages/stage2-embedding-three-zone.js +37 -0
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -0
- package/dist/conflict/stages/stage3-heuristic-regex.js +45 -0
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -0
- package/dist/conflict/stages/stage4-subject-predicate.js +31 -0
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -0
- package/dist/conflict/stages/stage5-defer-to-deep.js +22 -0
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -0
- package/dist/conflict/types.d.ts +222 -0
- package/dist/conflict/types.d.ts.map +1 -0
- package/dist/conflict/types.js +50 -0
- package/dist/conflict/types.js.map +1 -0
- package/dist/consolidator/budget.d.ts +1 -0
- package/dist/consolidator/budget.js +214 -0
- package/dist/consolidator/budget.js.map +1 -0
- package/dist/consolidator/decay.d.ts +35 -0
- package/dist/consolidator/decay.d.ts.map +1 -0
- package/dist/consolidator/decay.js +91 -0
- package/dist/consolidator/decay.js.map +1 -0
- package/dist/consolidator/dlq.js +58 -0
- package/dist/consolidator/dlq.js.map +1 -0
- package/dist/consolidator/errors.d.ts +54 -0
- package/dist/consolidator/errors.d.ts.map +1 -0
- package/dist/consolidator/errors.js +67 -0
- package/dist/consolidator/errors.js.map +1 -0
- package/dist/consolidator/idempotency.d.ts +1 -0
- package/dist/consolidator/idempotency.js +22 -0
- package/dist/consolidator/idempotency.js.map +1 -0
- package/dist/consolidator/index.d.ts +34 -0
- package/dist/consolidator/index.d.ts.map +1 -0
- package/dist/consolidator/index.js +163 -0
- package/dist/consolidator/index.js.map +1 -0
- package/dist/consolidator/lock.d.ts +2 -0
- package/dist/consolidator/lock.js +89 -0
- package/dist/consolidator/lock.js.map +1 -0
- package/dist/consolidator/noise-filter.d.ts +2 -0
- package/dist/consolidator/noise-filter.js +173 -0
- package/dist/consolidator/noise-filter.js.map +1 -0
- package/dist/consolidator/phases/deep.d.ts +4 -0
- package/dist/consolidator/phases/deep.js +198 -0
- package/dist/consolidator/phases/deep.js.map +1 -0
- package/dist/consolidator/phases/induce.d.ts +154 -0
- package/dist/consolidator/phases/induce.d.ts.map +1 -0
- package/dist/consolidator/phases/induce.js +253 -0
- package/dist/consolidator/phases/induce.js.map +1 -0
- package/dist/consolidator/phases/light.d.ts +4 -0
- package/dist/consolidator/phases/light.js +99 -0
- package/dist/consolidator/phases/light.js.map +1 -0
- package/dist/consolidator/phases/reflect.js +287 -0
- package/dist/consolidator/phases/reflect.js.map +1 -0
- package/dist/consolidator/phases/standard.d.ts +8 -0
- package/dist/consolidator/phases/standard.js +499 -0
- package/dist/consolidator/phases/standard.js.map +1 -0
- package/dist/consolidator/reconcile.js +192 -0
- package/dist/consolidator/reconcile.js.map +1 -0
- package/dist/consolidator/runtime.d.ts +62 -0
- package/dist/consolidator/runtime.d.ts.map +1 -0
- package/dist/consolidator/runtime.js +580 -0
- package/dist/consolidator/runtime.js.map +1 -0
- package/dist/consolidator/scheduler.d.ts +111 -0
- package/dist/consolidator/scheduler.d.ts.map +1 -0
- package/dist/consolidator/scheduler.js +71 -0
- package/dist/consolidator/scheduler.js.map +1 -0
- package/dist/consolidator/triggers.d.ts +40 -0
- package/dist/consolidator/triggers.d.ts.map +1 -0
- package/dist/consolidator/triggers.js +94 -0
- package/dist/consolidator/triggers.js.map +1 -0
- package/dist/consolidator/types.d.ts +397 -0
- package/dist/consolidator/types.d.ts.map +1 -0
- package/dist/consolidator/types.js +117 -0
- package/dist/consolidator/types.js.map +1 -0
- package/dist/context-engine/annotations.d.ts +118 -0
- package/dist/context-engine/annotations.d.ts.map +1 -0
- package/dist/context-engine/annotations.js +102 -0
- package/dist/context-engine/annotations.js.map +1 -0
- package/dist/context-engine/auto-recall.d.ts +52 -0
- package/dist/context-engine/auto-recall.d.ts.map +1 -0
- package/dist/context-engine/auto-recall.js +41 -0
- package/dist/context-engine/auto-recall.js.map +1 -0
- package/dist/context-engine/compaction/clear-tool-results.d.ts +55 -0
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/clear-tool-results.js +97 -0
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -0
- package/dist/context-engine/compaction/compactor.d.ts +52 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -0
- package/dist/context-engine/compaction/compactor.js +196 -0
- package/dist/context-engine/compaction/compactor.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts +15 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +27 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts +18 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +47 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts +15 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +32 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +42 -0
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -0
- package/dist/context-engine/compaction/index.d.ts +9 -0
- package/dist/context-engine/compaction/index.js +9 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +84 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.js +84 -0
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -0
- package/dist/context-engine/compaction/thresholds.d.ts +68 -0
- package/dist/context-engine/compaction/thresholds.d.ts.map +1 -0
- package/dist/context-engine/compaction/thresholds.js +63 -0
- package/dist/context-engine/compaction/thresholds.js.map +1 -0
- package/dist/context-engine/compaction/types.d.ts +202 -0
- package/dist/context-engine/compaction/types.d.ts.map +1 -0
- package/dist/context-engine/engine.d.ts +258 -0
- package/dist/context-engine/engine.d.ts.map +1 -0
- package/dist/context-engine/engine.js +424 -0
- package/dist/context-engine/engine.js.map +1 -0
- package/dist/context-engine/index.d.ts +68 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +24 -0
- package/dist/context-engine/locale-packs/en.d.ts +14 -0
- package/dist/context-engine/locale-packs/en.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/en.js +87 -0
- package/dist/context-engine/locale-packs/en.js.map +1 -0
- package/dist/context-engine/locale-packs/index.js +4 -0
- package/dist/context-engine/locale-packs/resolver.d.ts +41 -0
- package/dist/context-engine/locale-packs/resolver.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/resolver.js +119 -0
- package/dist/context-engine/locale-packs/resolver.js.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +119 -0
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.js +24 -0
- package/dist/context-engine/locale-packs/types.js.map +1 -0
- package/dist/context-engine/metadata.d.ts +40 -0
- package/dist/context-engine/metadata.d.ts.map +1 -0
- package/dist/context-engine/metadata.js +66 -0
- package/dist/context-engine/metadata.js.map +1 -0
- package/dist/context-engine/preambles/inbound-en.d.ts +20 -0
- package/dist/context-engine/preambles/inbound-en.d.ts.map +1 -0
- package/dist/context-engine/preambles/inbound-en.js +23 -0
- package/dist/context-engine/preambles/inbound-en.js.map +1 -0
- package/dist/context-engine/privacy-filter.d.ts +108 -0
- package/dist/context-engine/privacy-filter.d.ts.map +1 -0
- package/dist/context-engine/privacy-filter.js +113 -0
- package/dist/context-engine/privacy-filter.js.map +1 -0
- package/dist/context-engine/templates/base-en.d.ts +19 -0
- package/dist/context-engine/templates/base-en.d.ts.map +1 -0
- package/dist/context-engine/templates/base-en.js +22 -0
- package/dist/context-engine/templates/base-en.js.map +1 -0
- package/dist/context-engine/templates/composer.d.ts +55 -0
- package/dist/context-engine/templates/composer.d.ts.map +1 -0
- package/dist/context-engine/templates/composer.js +51 -0
- package/dist/context-engine/templates/composer.js.map +1 -0
- package/dist/context-engine/token-budget.d.ts +99 -0
- package/dist/context-engine/token-budget.d.ts.map +1 -0
- package/dist/context-engine/token-budget.js +211 -0
- package/dist/context-engine/token-budget.js.map +1 -0
- package/dist/context-engine/token-counter.d.ts +54 -0
- package/dist/context-engine/token-counter.d.ts.map +1 -0
- package/dist/context-engine/token-counter.js +81 -0
- package/dist/context-engine/token-counter.js.map +1 -0
- package/dist/errors/index.d.ts +171 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +206 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/facade.d.ts +278 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +265 -0
- package/dist/facade.js.map +1 -0
- package/dist/graph/entity-resolver.d.ts +135 -0
- package/dist/graph/entity-resolver.d.ts.map +1 -0
- package/dist/graph/entity-resolver.js +202 -0
- package/dist/graph/entity-resolver.js.map +1 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/contextualize.d.ts +14 -0
- package/dist/internal/contextualize.d.ts.map +1 -0
- package/dist/internal/contextualize.js +110 -0
- package/dist/internal/contextualize.js.map +1 -0
- package/dist/internal/embedder-binding.js +50 -0
- package/dist/internal/embedder-binding.js.map +1 -0
- package/dist/internal/id.js +24 -0
- package/dist/internal/id.js.map +1 -0
- package/dist/internal/injection-heuristics.js +64 -0
- package/dist/internal/injection-heuristics.js.map +1 -0
- package/dist/internal/spans.js +35 -0
- package/dist/internal/spans.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +742 -0
- package/dist/internal/storage-adapter.d.ts.map +1 -0
- package/dist/migration/embedder-migration.d.ts +124 -0
- package/dist/migration/embedder-migration.d.ts.map +1 -0
- package/dist/migration/embedder-migration.js +109 -0
- package/dist/migration/embedder-migration.js.map +1 -0
- package/dist/migration/index.d.ts +2 -0
- package/dist/migration/index.js +3 -0
- package/dist/search/explain.d.ts +74 -0
- package/dist/search/explain.d.ts.map +1 -0
- package/dist/search/explain.js +50 -0
- package/dist/search/explain.js.map +1 -0
- package/dist/search/index.d.ts +6 -0
- package/dist/search/index.js +6 -0
- package/dist/search/iterative.d.ts +223 -0
- package/dist/search/iterative.d.ts.map +1 -0
- package/dist/search/iterative.js +330 -0
- package/dist/search/iterative.js.map +1 -0
- package/dist/search/query-transform.d.ts +112 -0
- package/dist/search/query-transform.d.ts.map +1 -0
- package/dist/search/query-transform.js +184 -0
- package/dist/search/query-transform.js.map +1 -0
- package/dist/search/rrf.d.ts +95 -0
- package/dist/search/rrf.d.ts.map +1 -0
- package/dist/search/rrf.js +166 -0
- package/dist/search/rrf.js.map +1 -0
- package/dist/search/types.d.ts +50 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/tiers/episodic-memory.d.ts +142 -0
- package/dist/tiers/episodic-memory.d.ts.map +1 -0
- package/dist/tiers/episodic-memory.js +254 -0
- package/dist/tiers/episodic-memory.js.map +1 -0
- package/dist/tiers/index.d.ts +8 -0
- package/dist/tiers/index.js +9 -0
- package/dist/tiers/insight-memory.d.ts +71 -0
- package/dist/tiers/insight-memory.d.ts.map +1 -0
- package/dist/tiers/insight-memory.js +133 -0
- package/dist/tiers/insight-memory.js.map +1 -0
- package/dist/tiers/procedural-memory.d.ts +147 -0
- package/dist/tiers/procedural-memory.d.ts.map +1 -0
- package/dist/tiers/procedural-memory.js +267 -0
- package/dist/tiers/procedural-memory.js.map +1 -0
- package/dist/tiers/semantic-memory.d.ts +461 -0
- package/dist/tiers/semantic-memory.d.ts.map +1 -0
- package/dist/tiers/semantic-memory.js +631 -0
- package/dist/tiers/semantic-memory.js.map +1 -0
- package/dist/tiers/session-memory.d.ts +117 -0
- package/dist/tiers/session-memory.d.ts.map +1 -0
- package/dist/tiers/session-memory.js +163 -0
- package/dist/tiers/session-memory.js.map +1 -0
- package/dist/tiers/shared-memory.d.ts +29 -0
- package/dist/tiers/shared-memory.d.ts.map +1 -0
- package/dist/tiers/shared-memory.js +58 -0
- package/dist/tiers/shared-memory.js.map +1 -0
- package/dist/tiers/working-memory.d.ts +129 -0
- package/dist/tiers/working-memory.d.ts.map +1 -0
- package/dist/tiers/working-memory.js +229 -0
- package/dist/tiers/working-memory.js.map +1 -0
- package/dist/tools/block-tools.d.ts +98 -0
- package/dist/tools/block-tools.d.ts.map +1 -0
- package/dist/tools/block-tools.js +114 -0
- package/dist/tools/block-tools.js.map +1 -0
- package/dist/tools/fact-tools.d.ts +294 -0
- package/dist/tools/fact-tools.d.ts.map +1 -0
- package/dist/tools/fact-tools.js +293 -0
- package/dist/tools/fact-tools.js.map +1 -0
- package/dist/tools/index.d.ts +35 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/recall-tools.d.ts +209 -0
- package/dist/tools/recall-tools.d.ts.map +1 -0
- package/dist/tools/recall-tools.js +170 -0
- package/dist/tools/recall-tools.js.map +1 -0
- package/dist/tools/types.d.ts +40 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Provider, ProviderRequest } from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/search/iterative.d.ts
|
|
4
|
+
|
|
5
|
+
/** Default hard ceiling on total retrieval passes (gate + cap mandatory). */
|
|
6
|
+
declare const DEFAULT_MAX_ITERATIONS = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Absolute clamp on total retrieval passes, applied regardless of the
|
|
9
|
+
* caller's `maxIterations` — a latency guardrail so a mis-configured
|
|
10
|
+
* caller can never unbound the loop.
|
|
11
|
+
*/
|
|
12
|
+
declare const MAX_ITERATIONS_CEILING = 5;
|
|
13
|
+
/** Default score (0..1) at or above which a query is classified as hard. */
|
|
14
|
+
declare const DEFAULT_DIFFICULTY_THRESHOLD = 0.5;
|
|
15
|
+
/**
|
|
16
|
+
* Outcome of the heuristic difficulty gate. `hard` is the gating
|
|
17
|
+
* decision (`score >= threshold`); `signals` lists which heuristic
|
|
18
|
+
* categories fired, for explainability / tracing.
|
|
19
|
+
*
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
interface DifficultyAssessment {
|
|
23
|
+
readonly hard: boolean;
|
|
24
|
+
/** Aggregate difficulty score, clamped to `[0, 1]`. */
|
|
25
|
+
readonly score: number;
|
|
26
|
+
/** Heuristic categories that fired (`'multi-hop'`, `'temporal'`, …). */
|
|
27
|
+
readonly signals: ReadonlyArray<string>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Tuning for {@link assessQueryDifficulty}.
|
|
31
|
+
*
|
|
32
|
+
* @stable
|
|
33
|
+
*/
|
|
34
|
+
interface DifficultyGateOptions {
|
|
35
|
+
/** Minimum score (0..1) to classify the query as hard. Default 0.5. */
|
|
36
|
+
readonly threshold?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Score a query's retrieval difficulty with cheap, deterministic, fully
|
|
40
|
+
* **local** heuristics (no I/O). Conservative by design — it prefers to
|
|
41
|
+
* leave a query single-shot (`hard: false`) unless several
|
|
42
|
+
* multi-hop / temporal / comparison signals stack up, so the gate adds
|
|
43
|
+
* passes only where they are likely to help. Used by
|
|
44
|
+
* {@link runIterativeRetrieval} to decide whether to enter the loop.
|
|
45
|
+
*
|
|
46
|
+
* @stable
|
|
47
|
+
*/
|
|
48
|
+
declare function assessQueryDifficulty(query: string, options?: DifficultyGateOptions): DifficultyAssessment;
|
|
49
|
+
/**
|
|
50
|
+
* Verdict from grading a retrieved candidate set against a query.
|
|
51
|
+
*
|
|
52
|
+
* @stable
|
|
53
|
+
*/
|
|
54
|
+
interface RetrievalGrade {
|
|
55
|
+
/** Whether the retrieved memories suffice to answer the query. */
|
|
56
|
+
readonly sufficient: boolean;
|
|
57
|
+
/** Grader confidence, clamped to `[0, 1]`. */
|
|
58
|
+
readonly confidence: number;
|
|
59
|
+
/**
|
|
60
|
+
* A single better search query to try next, or `null` when the grader
|
|
61
|
+
* proposes none (the loop then stops / abstains).
|
|
62
|
+
*/
|
|
63
|
+
readonly reformulation: string | null;
|
|
64
|
+
/** Optional short rationale (never surfaced in spans). */
|
|
65
|
+
readonly reason?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Per-call options for a {@link RetrievalGrader}.
|
|
69
|
+
*
|
|
70
|
+
* @stable
|
|
71
|
+
*/
|
|
72
|
+
interface RetrievalGradeOptions {
|
|
73
|
+
/** Cancellation signal forwarded to the underlying provider call. */
|
|
74
|
+
readonly signal?: AbortSignal;
|
|
75
|
+
/**
|
|
76
|
+
* Reformulations already attempted (MRET-11). Surfaced to the grader
|
|
77
|
+
* as context so it can propose something genuinely new — the grade
|
|
78
|
+
* itself is ALWAYS judged against the original question, never a
|
|
79
|
+
* reformulation (a narrowed sub-query must not be declared
|
|
80
|
+
* "sufficient" while the original multi-hop question is not).
|
|
81
|
+
*/
|
|
82
|
+
readonly triedQueries?: ReadonlyArray<string>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Pluggable retrieval-grader seam consumed by the iterative loop. The
|
|
86
|
+
* built-in provider-backed implementation lives in
|
|
87
|
+
* {@link createProviderRetrievalGrader}; advanced callers can supply a
|
|
88
|
+
* bespoke grader (e.g. a deterministic heuristic).
|
|
89
|
+
*
|
|
90
|
+
* Implementations MUST degrade gracefully — return a "stop" grade rather
|
|
91
|
+
* than throw — so a grader failure never breaks recall.
|
|
92
|
+
*
|
|
93
|
+
* @stable
|
|
94
|
+
*/
|
|
95
|
+
interface RetrievalGrader {
|
|
96
|
+
grade(query: string, snippets: ReadonlyArray<string>, options?: RetrievalGradeOptions): Promise<RetrievalGrade>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* System prompt for the retrieval grader. Asks for a bare JSON verdict;
|
|
100
|
+
* {@link parseGrade} also tolerates a chatty model.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
declare const RETRIEVAL_GRADE_SYSTEM_PROMPT: string;
|
|
105
|
+
/**
|
|
106
|
+
* Build the grade request. Pure — no I/O. Temperature 0 so the verdict
|
|
107
|
+
* is as stable as the model allows.
|
|
108
|
+
*
|
|
109
|
+
* @stable
|
|
110
|
+
*/
|
|
111
|
+
declare function buildGradeRequest(query: string, snippets: ReadonlyArray<string>, options?: {
|
|
112
|
+
readonly maxTokens?: number;
|
|
113
|
+
readonly signal?: AbortSignal;
|
|
114
|
+
readonly triedQueries?: ReadonlyArray<string>;
|
|
115
|
+
}): ProviderRequest;
|
|
116
|
+
/**
|
|
117
|
+
* Parse a grader model output into a {@link RetrievalGrade}. Tolerates a
|
|
118
|
+
* bare JSON object, a fenced block, and chatty text around the object.
|
|
119
|
+
*
|
|
120
|
+
* **Fail-safe = stop.** When the verdict can't be read (undefined / empty
|
|
121
|
+
* / unparseable / missing `sufficient`), returns
|
|
122
|
+
* `{ sufficient: true, confidence: 0, reformulation: null }` so a broken
|
|
123
|
+
* grader degrades to a single pass rather than looping or falsely
|
|
124
|
+
* abstaining. A *parsed* `sufficient: false` (a real insufficiency
|
|
125
|
+
* verdict) is preserved.
|
|
126
|
+
*
|
|
127
|
+
* @stable
|
|
128
|
+
*/
|
|
129
|
+
declare function parseGrade(text: string | undefined): RetrievalGrade;
|
|
130
|
+
/**
|
|
131
|
+
* Wrap a {@link Provider} as a {@link RetrievalGrader}. **Resilient**: a
|
|
132
|
+
* provider error or unparseable response degrades to the
|
|
133
|
+
* {@link parseGrade} "stop" grade so grading never throws into the loop
|
|
134
|
+
* (and a flaky grader can't drive endless reformulation).
|
|
135
|
+
*
|
|
136
|
+
* @stable
|
|
137
|
+
*/
|
|
138
|
+
declare function createProviderRetrievalGrader(provider: Provider, options?: {
|
|
139
|
+
readonly maxTokens?: number;
|
|
140
|
+
}): RetrievalGrader;
|
|
141
|
+
/**
|
|
142
|
+
* Result of an iterative retrieval run.
|
|
143
|
+
*
|
|
144
|
+
* @stable
|
|
145
|
+
*/
|
|
146
|
+
interface IterativeRetrievalResult<H> {
|
|
147
|
+
/** Accumulated hits across all passes (deduped, in discovery order). */
|
|
148
|
+
readonly hits: ReadonlyArray<H>;
|
|
149
|
+
/** Number of retrieval passes performed (≥ 1). */
|
|
150
|
+
readonly iterations: number;
|
|
151
|
+
/** Difficulty-gate verdict (whether the loop was eligible to run). */
|
|
152
|
+
readonly gateHard: boolean;
|
|
153
|
+
/** Final sufficiency verdict. */
|
|
154
|
+
readonly sufficient: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* `true` when the loop exhausted its cap / ran out of reformulations
|
|
157
|
+
* while still judged insufficient — the caller should abstain rather
|
|
158
|
+
* than answer from `hits`.
|
|
159
|
+
*/
|
|
160
|
+
readonly abstained: boolean;
|
|
161
|
+
/** The sequence of queries tried (original first). */
|
|
162
|
+
readonly queries: ReadonlyArray<string>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Dependencies injected into {@link runIterativeRetrieval}. The loop does
|
|
166
|
+
* no I/O of its own — `retrieve` and `grader` own all side effects.
|
|
167
|
+
*
|
|
168
|
+
* @stable
|
|
169
|
+
*/
|
|
170
|
+
interface IterativeRetrievalDeps<H> {
|
|
171
|
+
/**
|
|
172
|
+
* Run one retrieval pass for `query`. `widen` is `true` on
|
|
173
|
+
* reformulation passes so the caller can broaden recall (e.g. enable
|
|
174
|
+
* P2-1 one-hop graph expansion).
|
|
175
|
+
*/
|
|
176
|
+
retrieve(query: string, widen: boolean, signal?: AbortSignal): Promise<ReadonlyArray<H>>;
|
|
177
|
+
/** Snippet shown to the grader for a hit. */
|
|
178
|
+
snippetOf(hit: H): string;
|
|
179
|
+
/** Stable id used to dedup hits across passes. */
|
|
180
|
+
idOf(hit: H): string;
|
|
181
|
+
/** Grader; `null` ⇒ single-shot (no grading, no provider call). */
|
|
182
|
+
grader: RetrievalGrader | null;
|
|
183
|
+
/**
|
|
184
|
+
* Re-fuse the per-pass hit lists into one ranked list (MRET-2).
|
|
185
|
+
* Receives one list per pass in pass order; the result feeds the
|
|
186
|
+
* final `maxResults` cut so a pass-2 find can outrank pass-1 noise.
|
|
187
|
+
* Absent ⇒ the loop falls back to round-robin interleaving (still
|
|
188
|
+
* strictly better than the old discovery-order cut).
|
|
189
|
+
*/
|
|
190
|
+
fuse?(lists: ReadonlyArray<ReadonlyArray<H>>): ReadonlyArray<H>;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Options for {@link runIterativeRetrieval}.
|
|
194
|
+
*
|
|
195
|
+
* @stable
|
|
196
|
+
*/
|
|
197
|
+
interface IterativeRetrievalOptions {
|
|
198
|
+
/** Total-pass cap; clamped to `[1, {@link MAX_ITERATIONS_CEILING}]`. */
|
|
199
|
+
readonly maxIterations?: number;
|
|
200
|
+
/** Difficulty-gate tuning. */
|
|
201
|
+
readonly difficulty?: DifficultyGateOptions;
|
|
202
|
+
/** Skip the heuristic gate and force the loop (still capped). */
|
|
203
|
+
readonly forceHard?: boolean;
|
|
204
|
+
/** Max snippets passed to the grader per pass. Default 8. */
|
|
205
|
+
readonly maxGradeSnippets?: number;
|
|
206
|
+
/** Cap on the returned hit count (omitted ⇒ all accumulated). */
|
|
207
|
+
readonly maxResults?: number;
|
|
208
|
+
readonly signal?: AbortSignal;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Run the gated grade-then-reformulate loop.
|
|
212
|
+
*
|
|
213
|
+
* Flow: assess difficulty → retrieve (pass 1) → if not hard *or* no
|
|
214
|
+
* grader, return single-shot → else grade; if sufficient, return; if
|
|
215
|
+
* weak and a reformulation is offered and the cap is not hit, retrieve
|
|
216
|
+
* again (widened) and re-grade; otherwise **abstain**.
|
|
217
|
+
*
|
|
218
|
+
* @stable
|
|
219
|
+
*/
|
|
220
|
+
declare function runIterativeRetrieval<H>(query: string, deps: IterativeRetrievalDeps<H>, options?: IterativeRetrievalOptions): Promise<IterativeRetrievalResult<H>>;
|
|
221
|
+
//#endregion
|
|
222
|
+
export { DEFAULT_DIFFICULTY_THRESHOLD, DEFAULT_MAX_ITERATIONS, DifficultyAssessment, DifficultyGateOptions, IterativeRetrievalDeps, IterativeRetrievalOptions, IterativeRetrievalResult, MAX_ITERATIONS_CEILING, RETRIEVAL_GRADE_SYSTEM_PROMPT, RetrievalGrade, RetrievalGradeOptions, RetrievalGrader, assessQueryDifficulty, buildGradeRequest, createProviderRetrievalGrader, parseGrade, runIterativeRetrieval };
|
|
223
|
+
//# sourceMappingURL=iterative.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iterative.d.ts","names":[],"sources":["../../src/search/iterative.ts"],"sourcesContent":[],"mappings":";;;;;AAmXiE,cA9UpD,sBAAA,GA8UoD,CAAA;;;;;;AAclD,cArVF,sBAAA,GAqVE,CAAA;;AAAkC,cAlVpC,4BAAA,GAkVoC,GAAA;;AAQjD;AAwBA;;;;;AAIW,UAnWM,oBAAA,CAmWN;EAAR,SAAA,IAAA,EAAA,OAAA;EAAO;;;oBA9VU;;;;;;;UAQH,qBAAA;;;;;;;;;;;;;;iBAqCD,qBAAA,0BAEL,wBACR;;;;;;UA8Cc,cAAA;;;;;;;;;;;;;;;;;;UAmBA,qBAAA;;oBAEG;;;;;;;;0BAQM;;;;;;;;;;;;;UAcT,eAAA;iCAGH,iCACA,wBACT,QAAQ;;;;;;;;cASA;;;;;;;iBAiBG,iBAAA,0BAEJ;;oBAGU;0BACM;IAEzB;;;;;;;;;;;;;;iBAkCa,UAAA,4BAAsC;;;;;;;;;iBA4BtC,6BAAA,WACJ;;IAET;;;;;;UA4Bc;;iBAEA,cAAc;;;;;;;;;;;;;;oBAcX;;;;;;;;UASH;;;;;;mDAMkC,cAAc,QAAQ,cAAc;;iBAEtE;;YAEL;;UAEF;;;;;;;;eAQK,cAAc,cAAc,MAAM,cAAc;;;;;;;UAQ9C,yBAAA;;;;wBAIO;;;;;;;oBAOJ;;;;;;;;;;;;iBAaE,8CAEd,uBAAuB,cACpB,4BACR,QAAQ,yBAAyB"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
//#region src/search/iterative.ts
|
|
2
|
+
/** Default hard ceiling on total retrieval passes (gate + cap mandatory). */
|
|
3
|
+
const DEFAULT_MAX_ITERATIONS = 3;
|
|
4
|
+
/**
|
|
5
|
+
* Absolute clamp on total retrieval passes, applied regardless of the
|
|
6
|
+
* caller's `maxIterations` — a latency guardrail so a mis-configured
|
|
7
|
+
* caller can never unbound the loop.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_ITERATIONS_CEILING = 5;
|
|
10
|
+
/** Default score (0..1) at or above which a query is classified as hard. */
|
|
11
|
+
const DEFAULT_DIFFICULTY_THRESHOLD = .5;
|
|
12
|
+
/** Default output-token ceiling for a single grade call. */
|
|
13
|
+
const DEFAULT_GRADE_MAX_TOKENS = 256;
|
|
14
|
+
/** Default number of retrieved snippets shown to the grader per pass. */
|
|
15
|
+
const DEFAULT_MAX_GRADE_SNIPPETS = 8;
|
|
16
|
+
/** Relational / referential markers implying a hop to another entity. */
|
|
17
|
+
const RE_REL_PRONOUN = /\b(?:who|whom|whose|which)\b/iu;
|
|
18
|
+
const RE_OF_CHAIN = /\bthe\s+\w+\s+of\b/iu;
|
|
19
|
+
const RE_POSSESSIVE = /\b[\w-]+'s\b/giu;
|
|
20
|
+
const RE_DESC_REF = /\bthe\s+(?:person|people|place|thing|one|company|team|book|restaurant|project|guy|woman|man)\b/iu;
|
|
21
|
+
/** Temporal ordering / relative-time markers. */
|
|
22
|
+
const RE_TEMPORAL = /\b(?:before|after|when|while|during|since|until|first|last|earliest|latest|previously|originally|ago|prior|then|recently|used\s+to)\b/iu;
|
|
23
|
+
/** Comparison / superlative markers. */
|
|
24
|
+
const RE_COMPARISON = /\b(?:most|least|more|less|fewer|greater|biggest|largest|smallest|oldest|newest|best|worst|highest|lowest|compared?|versus|vs\.?|between)\b/iu;
|
|
25
|
+
/** Clause-joining conjunctions used to count clauses. */
|
|
26
|
+
const RE_CONJUNCTION = /\b(?:and|or|but|then)\b/giu;
|
|
27
|
+
const W_MULTI_HOP = .4;
|
|
28
|
+
const W_TEMPORAL = .3;
|
|
29
|
+
const W_COMPARISON = .3;
|
|
30
|
+
const W_MULTI_CLAUSE = .2;
|
|
31
|
+
const W_LONG = .15;
|
|
32
|
+
const LONG_WORD_COUNT = 12;
|
|
33
|
+
/**
|
|
34
|
+
* Score a query's retrieval difficulty with cheap, deterministic, fully
|
|
35
|
+
* **local** heuristics (no I/O). Conservative by design — it prefers to
|
|
36
|
+
* leave a query single-shot (`hard: false`) unless several
|
|
37
|
+
* multi-hop / temporal / comparison signals stack up, so the gate adds
|
|
38
|
+
* passes only where they are likely to help. Used by
|
|
39
|
+
* {@link runIterativeRetrieval} to decide whether to enter the loop.
|
|
40
|
+
*
|
|
41
|
+
* @stable
|
|
42
|
+
*/
|
|
43
|
+
function assessQueryDifficulty(query, options = {}) {
|
|
44
|
+
const threshold = options.threshold ?? DEFAULT_DIFFICULTY_THRESHOLD;
|
|
45
|
+
const signals = [];
|
|
46
|
+
let score = 0;
|
|
47
|
+
const possessiveCount = (query.match(RE_POSSESSIVE) ?? []).length;
|
|
48
|
+
if (RE_REL_PRONOUN.test(query) || RE_OF_CHAIN.test(query) || RE_DESC_REF.test(query) || possessiveCount >= 2) {
|
|
49
|
+
signals.push("multi-hop");
|
|
50
|
+
score += W_MULTI_HOP;
|
|
51
|
+
}
|
|
52
|
+
if (RE_TEMPORAL.test(query)) {
|
|
53
|
+
signals.push("temporal");
|
|
54
|
+
score += W_TEMPORAL;
|
|
55
|
+
}
|
|
56
|
+
if (RE_COMPARISON.test(query)) {
|
|
57
|
+
signals.push("comparison");
|
|
58
|
+
score += W_COMPARISON;
|
|
59
|
+
}
|
|
60
|
+
if ((query.match(RE_CONJUNCTION) ?? []).length + countCommas(query) >= 2 || questionMarkCount(query) >= 2) {
|
|
61
|
+
signals.push("multi-clause");
|
|
62
|
+
score += W_MULTI_CLAUSE;
|
|
63
|
+
}
|
|
64
|
+
if (wordCount(query) >= LONG_WORD_COUNT) {
|
|
65
|
+
signals.push("long");
|
|
66
|
+
score += W_LONG;
|
|
67
|
+
}
|
|
68
|
+
const clamped = Math.min(1, score);
|
|
69
|
+
return {
|
|
70
|
+
hard: clamped >= threshold,
|
|
71
|
+
score: clamped,
|
|
72
|
+
signals
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* System prompt for the retrieval grader. Asks for a bare JSON verdict;
|
|
77
|
+
* {@link parseGrade} also tolerates a chatty model.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
const RETRIEVAL_GRADE_SYSTEM_PROMPT = "You are a retrieval grader for a personal memory store. Given a question and a numbered list of retrieved memories, judge whether they are SUFFICIENT to answer the question correctly and completely. Respond with ONLY a JSON object: {\"sufficient\": boolean, \"confidence\": number between 0 and 1, \"reformulation\": string or null, \"reason\": string}. Set \"sufficient\" to true only if the memories actually contain the answer — do not guess. When insufficient, set \"reformulation\" to a single alternative search query likely to retrieve the missing information (vary entities, time frame, or specificity); otherwise null. Output only the JSON object — no prose, no markdown.";
|
|
82
|
+
/**
|
|
83
|
+
* Build the grade request. Pure — no I/O. Temperature 0 so the verdict
|
|
84
|
+
* is as stable as the model allows.
|
|
85
|
+
*
|
|
86
|
+
* @stable
|
|
87
|
+
*/
|
|
88
|
+
function buildGradeRequest(query, snippets, options = {}) {
|
|
89
|
+
return {
|
|
90
|
+
messages: [{
|
|
91
|
+
role: "user",
|
|
92
|
+
content: `Question: ${query}\n\nRetrieved memories:\n${snippets.length === 0 ? "(none)" : snippets.map((s, i) => `[${i + 1}] ${s}`).join("\n")}${options.triedQueries !== void 0 && options.triedQueries.length > 0 ? `\n\nQueries already tried (do not repeat them): ${options.triedQueries.join(" | ")}` : ""}\n\nGrade whether these memories are sufficient to answer the question. If not, propose a single better search query.`
|
|
93
|
+
}],
|
|
94
|
+
systemMessage: RETRIEVAL_GRADE_SYSTEM_PROMPT,
|
|
95
|
+
temperature: 0,
|
|
96
|
+
maxTokens: options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS,
|
|
97
|
+
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
98
|
+
outputType: { kind: "structured" }
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse a grader model output into a {@link RetrievalGrade}. Tolerates a
|
|
103
|
+
* bare JSON object, a fenced block, and chatty text around the object.
|
|
104
|
+
*
|
|
105
|
+
* **Fail-safe = stop.** When the verdict can't be read (undefined / empty
|
|
106
|
+
* / unparseable / missing `sufficient`), returns
|
|
107
|
+
* `{ sufficient: true, confidence: 0, reformulation: null }` so a broken
|
|
108
|
+
* grader degrades to a single pass rather than looping or falsely
|
|
109
|
+
* abstaining. A *parsed* `sufficient: false` (a real insufficiency
|
|
110
|
+
* verdict) is preserved.
|
|
111
|
+
*
|
|
112
|
+
* @stable
|
|
113
|
+
*/
|
|
114
|
+
function parseGrade(text) {
|
|
115
|
+
const stop = {
|
|
116
|
+
sufficient: true,
|
|
117
|
+
confidence: 0,
|
|
118
|
+
reformulation: null
|
|
119
|
+
};
|
|
120
|
+
if (text === void 0) return stop;
|
|
121
|
+
const stripped = stripFence(text).trim();
|
|
122
|
+
if (stripped.length === 0) return stop;
|
|
123
|
+
const obj = tryParseObject(stripped);
|
|
124
|
+
if (obj === null) return stop;
|
|
125
|
+
const sufficient = coerceBool(obj.sufficient);
|
|
126
|
+
if (sufficient === null) return stop;
|
|
127
|
+
const confidence = coerceConfidence(obj.confidence, sufficient);
|
|
128
|
+
const reformulation = sufficient ? null : coerceReformulation(obj.reformulation);
|
|
129
|
+
const reason = coerceReason(obj.reason);
|
|
130
|
+
return {
|
|
131
|
+
sufficient,
|
|
132
|
+
confidence,
|
|
133
|
+
reformulation,
|
|
134
|
+
...reason !== void 0 ? { reason } : {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Wrap a {@link Provider} as a {@link RetrievalGrader}. **Resilient**: a
|
|
139
|
+
* provider error or unparseable response degrades to the
|
|
140
|
+
* {@link parseGrade} "stop" grade so grading never throws into the loop
|
|
141
|
+
* (and a flaky grader can't drive endless reformulation).
|
|
142
|
+
*
|
|
143
|
+
* @stable
|
|
144
|
+
*/
|
|
145
|
+
function createProviderRetrievalGrader(provider, options = {}) {
|
|
146
|
+
const maxTokens = options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS;
|
|
147
|
+
return { async grade(query, snippets, opts = {}) {
|
|
148
|
+
try {
|
|
149
|
+
const request = buildGradeRequest(query, snippets, {
|
|
150
|
+
maxTokens,
|
|
151
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {},
|
|
152
|
+
...opts.triedQueries !== void 0 ? { triedQueries: opts.triedQueries } : {}
|
|
153
|
+
});
|
|
154
|
+
return parseGrade((await provider.generate(request)).text);
|
|
155
|
+
} catch {
|
|
156
|
+
return {
|
|
157
|
+
sufficient: true,
|
|
158
|
+
confidence: 0,
|
|
159
|
+
reformulation: null
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
} };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Run the gated grade-then-reformulate loop.
|
|
166
|
+
*
|
|
167
|
+
* Flow: assess difficulty → retrieve (pass 1) → if not hard *or* no
|
|
168
|
+
* grader, return single-shot → else grade; if sufficient, return; if
|
|
169
|
+
* weak and a reformulation is offered and the cap is not hit, retrieve
|
|
170
|
+
* again (widened) and re-grade; otherwise **abstain**.
|
|
171
|
+
*
|
|
172
|
+
* @stable
|
|
173
|
+
*/
|
|
174
|
+
async function runIterativeRetrieval(query, deps, options = {}) {
|
|
175
|
+
const gate = options.forceHard === true ? {
|
|
176
|
+
hard: true,
|
|
177
|
+
score: 1,
|
|
178
|
+
signals: ["forced"]
|
|
179
|
+
} : assessQueryDifficulty(query, options.difficulty ?? {});
|
|
180
|
+
const cap = clampIterations(options.maxIterations);
|
|
181
|
+
const maxSnippets = Math.max(1, options.maxGradeSnippets ?? DEFAULT_MAX_GRADE_SNIPPETS);
|
|
182
|
+
const signal = options.signal;
|
|
183
|
+
const passLists = [];
|
|
184
|
+
const accumulate = (hits) => {
|
|
185
|
+
passLists.push([...hits]);
|
|
186
|
+
};
|
|
187
|
+
const queries = [query];
|
|
188
|
+
const tried = new Set([query.trim().toLowerCase()]);
|
|
189
|
+
accumulate(await deps.retrieve(query, false, signal));
|
|
190
|
+
const grader = deps.grader;
|
|
191
|
+
if (!gate.hard || grader === null) return finalize(deps, passLists, 1, gate.hard, true, false, queries, options.maxResults);
|
|
192
|
+
let passes = 1;
|
|
193
|
+
for (;;) {
|
|
194
|
+
const window = interleave(passLists, maxSnippets, deps.idOf).map(deps.snippetOf);
|
|
195
|
+
const grade = await grader.grade(query, window, {
|
|
196
|
+
...signal !== void 0 ? { signal } : {},
|
|
197
|
+
...queries.length > 1 ? { triedQueries: queries.slice(1) } : {}
|
|
198
|
+
});
|
|
199
|
+
if (grade.sufficient) return finalize(deps, passLists, passes, true, true, false, queries, options.maxResults);
|
|
200
|
+
if (passes >= cap) break;
|
|
201
|
+
const next = normalizeReformulation(grade.reformulation, tried);
|
|
202
|
+
if (next === null) break;
|
|
203
|
+
passes += 1;
|
|
204
|
+
queries.push(next);
|
|
205
|
+
tried.add(next.toLowerCase());
|
|
206
|
+
accumulate(await deps.retrieve(next, true, signal));
|
|
207
|
+
}
|
|
208
|
+
return finalize(deps, passLists, passes, true, false, true, queries, options.maxResults);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Round-robin interleave of per-pass lists, deduped by id: hit 1 of
|
|
212
|
+
* every pass, then hit 2 of every pass, … — guarantees the latest
|
|
213
|
+
* pass's top hits enter a window of any size.
|
|
214
|
+
*/
|
|
215
|
+
function interleave(lists, limit, idOf) {
|
|
216
|
+
const out = [];
|
|
217
|
+
const seen = /* @__PURE__ */ new Set();
|
|
218
|
+
const longest = lists.reduce((max, l) => Math.max(max, l.length), 0);
|
|
219
|
+
for (let rank = 0; rank < longest && out.length < limit; rank += 1) for (const list of lists) {
|
|
220
|
+
const hit = list[rank];
|
|
221
|
+
if (hit === void 0) continue;
|
|
222
|
+
const id = idOf(hit);
|
|
223
|
+
if (seen.has(id)) continue;
|
|
224
|
+
seen.add(id);
|
|
225
|
+
out.push(hit);
|
|
226
|
+
if (out.length >= limit) break;
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
function finalize(deps, passLists, iterations, gateHard, sufficient, abstained, queries, maxResults) {
|
|
231
|
+
const ranked = typeof deps.fuse === "function" ? dedupBy(deps.fuse(passLists), deps.idOf) : interleave(passLists, Number.POSITIVE_INFINITY, deps.idOf);
|
|
232
|
+
return {
|
|
233
|
+
hits: maxResults !== void 0 && maxResults >= 0 ? ranked.slice(0, maxResults) : ranked.slice(),
|
|
234
|
+
iterations,
|
|
235
|
+
gateHard,
|
|
236
|
+
sufficient,
|
|
237
|
+
abstained,
|
|
238
|
+
queries: [...queries]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function dedupBy(hits, idOf) {
|
|
242
|
+
const seen = /* @__PURE__ */ new Set();
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const hit of hits) {
|
|
245
|
+
const id = idOf(hit);
|
|
246
|
+
if (seen.has(id)) continue;
|
|
247
|
+
seen.add(id);
|
|
248
|
+
out.push(hit);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function clampIterations(value) {
|
|
253
|
+
const n = value ?? DEFAULT_MAX_ITERATIONS;
|
|
254
|
+
if (!Number.isFinite(n)) return DEFAULT_MAX_ITERATIONS;
|
|
255
|
+
return Math.min(MAX_ITERATIONS_CEILING, Math.max(1, Math.floor(n)));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Normalise a proposed reformulation: trim, drop empties, and reject a
|
|
259
|
+
* query already tried (case-insensitive) so the loop never re-treads and
|
|
260
|
+
* cannot spin.
|
|
261
|
+
*/
|
|
262
|
+
function normalizeReformulation(raw, tried) {
|
|
263
|
+
if (raw === null) return null;
|
|
264
|
+
const trimmed = raw.trim();
|
|
265
|
+
if (trimmed.length === 0) return null;
|
|
266
|
+
if (tried.has(trimmed.toLowerCase())) return null;
|
|
267
|
+
return trimmed;
|
|
268
|
+
}
|
|
269
|
+
function wordCount(text) {
|
|
270
|
+
const trimmed = text.trim();
|
|
271
|
+
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/u).length;
|
|
272
|
+
}
|
|
273
|
+
function countCommas(text) {
|
|
274
|
+
return (text.match(/,/gu) ?? []).length;
|
|
275
|
+
}
|
|
276
|
+
function questionMarkCount(text) {
|
|
277
|
+
return (text.match(/\?/gu) ?? []).length;
|
|
278
|
+
}
|
|
279
|
+
function coerceBool(value) {
|
|
280
|
+
if (typeof value === "boolean") return value;
|
|
281
|
+
if (typeof value === "string") {
|
|
282
|
+
const v = value.trim().toLowerCase();
|
|
283
|
+
if (v === "true" || v === "yes" || v === "sufficient") return true;
|
|
284
|
+
if (v === "false" || v === "no" || v === "insufficient") return false;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function coerceConfidence(value, sufficient) {
|
|
289
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.min(1, Math.max(0, value));
|
|
290
|
+
return sufficient ? .8 : .3;
|
|
291
|
+
}
|
|
292
|
+
function coerceReformulation(value) {
|
|
293
|
+
if (typeof value !== "string") return null;
|
|
294
|
+
const trimmed = value.trim();
|
|
295
|
+
if (trimmed.length === 0) return null;
|
|
296
|
+
return trimmed.length > 1024 ? trimmed.slice(0, 1024) : trimmed;
|
|
297
|
+
}
|
|
298
|
+
function coerceReason(value) {
|
|
299
|
+
if (typeof value !== "string") return void 0;
|
|
300
|
+
const trimmed = value.trim();
|
|
301
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
302
|
+
}
|
|
303
|
+
function tryParseObject(text) {
|
|
304
|
+
const direct = tryJson(text);
|
|
305
|
+
if (isRecord(direct)) return direct;
|
|
306
|
+
const start = text.indexOf("{");
|
|
307
|
+
const end = text.lastIndexOf("}");
|
|
308
|
+
if (start >= 0 && end > start) {
|
|
309
|
+
const sliced = tryJson(text.slice(start, end + 1));
|
|
310
|
+
if (isRecord(sliced)) return sliced;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function tryJson(text) {
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(text);
|
|
317
|
+
} catch {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function stripFence(text) {
|
|
322
|
+
return /^```[^\n]*\n([\s\S]*?)\n```/u.exec(text.trim())?.[1] ?? text;
|
|
323
|
+
}
|
|
324
|
+
function isRecord(value) {
|
|
325
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
export { DEFAULT_DIFFICULTY_THRESHOLD, DEFAULT_MAX_ITERATIONS, MAX_ITERATIONS_CEILING, RETRIEVAL_GRADE_SYSTEM_PROMPT, assessQueryDifficulty, buildGradeRequest, createProviderRetrievalGrader, parseGrade, runIterativeRetrieval };
|
|
330
|
+
//# sourceMappingURL=iterative.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iterative.js","names":["signals: string[]","stop: RetrievalGrade","passLists: Array<ReadonlyArray<H>>","queries: string[]","out: H[]"],"sources":["../../src/search/iterative.ts"],"sourcesContent":["/**\n * Agentic / iterative retrieval for hard queries (P2-4) — a **gated**,\n * CRAG/Self-RAG-style grade-then-reformulate loop.\n *\n * Some multi-hop / temporal questions can't be answered from a single\n * retrieval pass; the naive system returns a confident-but-wrong answer\n * instead of retrieving again. This module adds a small loop: grade the\n * retrieved memories for sufficiency and, when weak, reformulate the\n * query and retrieve once more (optionally widening to the P2-1 graph) —\n * up to a hard iteration cap, then **abstain** rather than confabulate.\n *\n * Both CRAG (arXiv:2401.15884) and Self-RAG (arXiv:2310.11511) add\n * latency/cost, so the literature's advice — and this module's design —\n * is to *gate* the loop, not make it default:\n *\n * 1. A cheap, **local** {@link assessQueryDifficulty} heuristic decides\n * whether a query is even a candidate; simple lookups never enter\n * the loop (no provider call) and stay single-shot.\n * 2. The grade/reformulate step needs a {@link RetrievalGrader}; with\n * none configured the loop degrades to a single retrieval pass. The\n * only provider-backed grader, {@link createProviderRetrievalGrader},\n * is **resilient**: any provider error or unparseable output degrades\n * to a \"stop\" grade rather than throwing or looping forever.\n * 3. A mandatory iteration cap bounds the worst case regardless of the\n * grader.\n *\n * The module is provider-agnostic — it imports only `@graphorin/core`\n * types and never performs I/O itself; {@link runIterativeRetrieval} is a\n * pure orchestrator over an injected `retrieve` function + grader, so the\n * loop is unit-testable without a store or a model.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\n\n/** Default hard ceiling on total retrieval passes (gate + cap mandatory). */\nexport const DEFAULT_MAX_ITERATIONS = 3;\n\n/**\n * Absolute clamp on total retrieval passes, applied regardless of the\n * caller's `maxIterations` — a latency guardrail so a mis-configured\n * caller can never unbound the loop.\n */\nexport const MAX_ITERATIONS_CEILING = 5;\n\n/** Default score (0..1) at or above which a query is classified as hard. */\nexport const DEFAULT_DIFFICULTY_THRESHOLD = 0.5;\n\n/** Default output-token ceiling for a single grade call. */\nconst DEFAULT_GRADE_MAX_TOKENS = 256;\n\n/** Default number of retrieved snippets shown to the grader per pass. */\nconst DEFAULT_MAX_GRADE_SNIPPETS = 8;\n\n// ---------------------------------------------------------------------------\n// Difficulty gate (pure, local — no I/O, no provider)\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome of the heuristic difficulty gate. `hard` is the gating\n * decision (`score >= threshold`); `signals` lists which heuristic\n * categories fired, for explainability / tracing.\n *\n * @stable\n */\nexport interface DifficultyAssessment {\n readonly hard: boolean;\n /** Aggregate difficulty score, clamped to `[0, 1]`. */\n readonly score: number;\n /** Heuristic categories that fired (`'multi-hop'`, `'temporal'`, …). */\n readonly signals: ReadonlyArray<string>;\n}\n\n/**\n * Tuning for {@link assessQueryDifficulty}.\n *\n * @stable\n */\nexport interface DifficultyGateOptions {\n /** Minimum score (0..1) to classify the query as hard. Default 0.5. */\n readonly threshold?: number;\n}\n\n/** Relational / referential markers implying a hop to another entity. */\nconst RE_REL_PRONOUN = /\\b(?:who|whom|whose|which)\\b/iu;\nconst RE_OF_CHAIN = /\\bthe\\s+\\w+\\s+of\\b/iu;\nconst RE_POSSESSIVE = /\\b[\\w-]+'s\\b/giu;\nconst RE_DESC_REF =\n /\\bthe\\s+(?:person|people|place|thing|one|company|team|book|restaurant|project|guy|woman|man)\\b/iu;\n/** Temporal ordering / relative-time markers. */\nconst RE_TEMPORAL =\n /\\b(?:before|after|when|while|during|since|until|first|last|earliest|latest|previously|originally|ago|prior|then|recently|used\\s+to)\\b/iu;\n/** Comparison / superlative markers. */\nconst RE_COMPARISON =\n /\\b(?:most|least|more|less|fewer|greater|biggest|largest|smallest|oldest|newest|best|worst|highest|lowest|compared?|versus|vs\\.?|between)\\b/iu;\n/** Clause-joining conjunctions used to count clauses. */\nconst RE_CONJUNCTION = /\\b(?:and|or|but|then)\\b/giu;\n\nconst W_MULTI_HOP = 0.4;\nconst W_TEMPORAL = 0.3;\nconst W_COMPARISON = 0.3;\nconst W_MULTI_CLAUSE = 0.2;\nconst W_LONG = 0.15;\nconst LONG_WORD_COUNT = 12;\n\n/**\n * Score a query's retrieval difficulty with cheap, deterministic, fully\n * **local** heuristics (no I/O). Conservative by design — it prefers to\n * leave a query single-shot (`hard: false`) unless several\n * multi-hop / temporal / comparison signals stack up, so the gate adds\n * passes only where they are likely to help. Used by\n * {@link runIterativeRetrieval} to decide whether to enter the loop.\n *\n * @stable\n */\nexport function assessQueryDifficulty(\n query: string,\n options: DifficultyGateOptions = {},\n): DifficultyAssessment {\n const threshold = options.threshold ?? DEFAULT_DIFFICULTY_THRESHOLD;\n const signals: string[] = [];\n let score = 0;\n\n const possessiveCount = (query.match(RE_POSSESSIVE) ?? []).length;\n const multiHop =\n RE_REL_PRONOUN.test(query) ||\n RE_OF_CHAIN.test(query) ||\n RE_DESC_REF.test(query) ||\n possessiveCount >= 2;\n if (multiHop) {\n signals.push('multi-hop');\n score += W_MULTI_HOP;\n }\n if (RE_TEMPORAL.test(query)) {\n signals.push('temporal');\n score += W_TEMPORAL;\n }\n if (RE_COMPARISON.test(query)) {\n signals.push('comparison');\n score += W_COMPARISON;\n }\n const clauseSignals = (query.match(RE_CONJUNCTION) ?? []).length + countCommas(query);\n if (clauseSignals >= 2 || questionMarkCount(query) >= 2) {\n signals.push('multi-clause');\n score += W_MULTI_CLAUSE;\n }\n if (wordCount(query) >= LONG_WORD_COUNT) {\n signals.push('long');\n score += W_LONG;\n }\n\n const clamped = Math.min(1, score);\n return { hard: clamped >= threshold, score: clamped, signals };\n}\n\n// ---------------------------------------------------------------------------\n// Retrieval grader seam (provider-agnostic)\n// ---------------------------------------------------------------------------\n\n/**\n * Verdict from grading a retrieved candidate set against a query.\n *\n * @stable\n */\nexport interface RetrievalGrade {\n /** Whether the retrieved memories suffice to answer the query. */\n readonly sufficient: boolean;\n /** Grader confidence, clamped to `[0, 1]`. */\n readonly confidence: number;\n /**\n * A single better search query to try next, or `null` when the grader\n * proposes none (the loop then stops / abstains).\n */\n readonly reformulation: string | null;\n /** Optional short rationale (never surfaced in spans). */\n readonly reason?: string;\n}\n\n/**\n * Per-call options for a {@link RetrievalGrader}.\n *\n * @stable\n */\nexport interface RetrievalGradeOptions {\n /** Cancellation signal forwarded to the underlying provider call. */\n readonly signal?: AbortSignal;\n /**\n * Reformulations already attempted (MRET-11). Surfaced to the grader\n * as context so it can propose something genuinely new — the grade\n * itself is ALWAYS judged against the original question, never a\n * reformulation (a narrowed sub-query must not be declared\n * \"sufficient\" while the original multi-hop question is not).\n */\n readonly triedQueries?: ReadonlyArray<string>;\n}\n\n/**\n * Pluggable retrieval-grader seam consumed by the iterative loop. The\n * built-in provider-backed implementation lives in\n * {@link createProviderRetrievalGrader}; advanced callers can supply a\n * bespoke grader (e.g. a deterministic heuristic).\n *\n * Implementations MUST degrade gracefully — return a \"stop\" grade rather\n * than throw — so a grader failure never breaks recall.\n *\n * @stable\n */\nexport interface RetrievalGrader {\n grade(\n query: string,\n snippets: ReadonlyArray<string>,\n options?: RetrievalGradeOptions,\n ): Promise<RetrievalGrade>;\n}\n\n/**\n * System prompt for the retrieval grader. Asks for a bare JSON verdict;\n * {@link parseGrade} also tolerates a chatty model.\n *\n * @internal\n */\nexport const RETRIEVAL_GRADE_SYSTEM_PROMPT =\n 'You are a retrieval grader for a personal memory store. Given a question and a ' +\n 'numbered list of retrieved memories, judge whether they are SUFFICIENT to answer ' +\n 'the question correctly and completely. Respond with ONLY a JSON object: ' +\n '{\"sufficient\": boolean, \"confidence\": number between 0 and 1, \"reformulation\": ' +\n 'string or null, \"reason\": string}. Set \"sufficient\" to true only if the memories ' +\n 'actually contain the answer — do not guess. When insufficient, set \"reformulation\" ' +\n 'to a single alternative search query likely to retrieve the missing information ' +\n '(vary entities, time frame, or specificity); otherwise null. Output only the JSON ' +\n 'object — no prose, no markdown.';\n\n/**\n * Build the grade request. Pure — no I/O. Temperature 0 so the verdict\n * is as stable as the model allows.\n *\n * @stable\n */\nexport function buildGradeRequest(\n query: string,\n snippets: ReadonlyArray<string>,\n options: {\n readonly maxTokens?: number;\n readonly signal?: AbortSignal;\n readonly triedQueries?: ReadonlyArray<string>;\n } = {},\n): ProviderRequest {\n const body =\n snippets.length === 0 ? '(none)' : snippets.map((s, i) => `[${i + 1}] ${s}`).join('\\n');\n const tried =\n options.triedQueries !== undefined && options.triedQueries.length > 0\n ? `\\n\\nQueries already tried (do not repeat them): ${options.triedQueries.join(' | ')}`\n : '';\n const content =\n `Question: ${query}\\n\\nRetrieved memories:\\n${body}${tried}\\n\\n` +\n 'Grade whether these memories are sufficient to answer the question. If not, ' +\n 'propose a single better search query.';\n return {\n messages: [{ role: 'user', content }],\n systemMessage: RETRIEVAL_GRADE_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Parse a grader model output into a {@link RetrievalGrade}. Tolerates a\n * bare JSON object, a fenced block, and chatty text around the object.\n *\n * **Fail-safe = stop.** When the verdict can't be read (undefined / empty\n * / unparseable / missing `sufficient`), returns\n * `{ sufficient: true, confidence: 0, reformulation: null }` so a broken\n * grader degrades to a single pass rather than looping or falsely\n * abstaining. A *parsed* `sufficient: false` (a real insufficiency\n * verdict) is preserved.\n *\n * @stable\n */\nexport function parseGrade(text: string | undefined): RetrievalGrade {\n const stop: RetrievalGrade = { sufficient: true, confidence: 0, reformulation: null };\n if (text === undefined) return stop;\n const stripped = stripFence(text).trim();\n if (stripped.length === 0) return stop;\n const obj = tryParseObject(stripped);\n if (obj === null) return stop;\n const sufficient = coerceBool(obj.sufficient);\n if (sufficient === null) return stop;\n const confidence = coerceConfidence(obj.confidence, sufficient);\n const reformulation = sufficient ? null : coerceReformulation(obj.reformulation);\n const reason = coerceReason(obj.reason);\n return {\n sufficient,\n confidence,\n reformulation,\n ...(reason !== undefined ? { reason } : {}),\n };\n}\n\n/**\n * Wrap a {@link Provider} as a {@link RetrievalGrader}. **Resilient**: a\n * provider error or unparseable response degrades to the\n * {@link parseGrade} \"stop\" grade so grading never throws into the loop\n * (and a flaky grader can't drive endless reformulation).\n *\n * @stable\n */\nexport function createProviderRetrievalGrader(\n provider: Provider,\n options: { readonly maxTokens?: number } = {},\n): RetrievalGrader {\n const maxTokens = options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS;\n return {\n async grade(query, snippets, opts = {}): Promise<RetrievalGrade> {\n try {\n const request = buildGradeRequest(query, snippets, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.triedQueries !== undefined ? { triedQueries: opts.triedQueries } : {}),\n });\n const response = await provider.generate(request);\n return parseGrade(response.text);\n } catch {\n return { sufficient: true, confidence: 0, reformulation: null };\n }\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// The bounded loop (pure orchestration over injected retrieve + grader)\n// ---------------------------------------------------------------------------\n\n/**\n * Result of an iterative retrieval run.\n *\n * @stable\n */\nexport interface IterativeRetrievalResult<H> {\n /** Accumulated hits across all passes (deduped, in discovery order). */\n readonly hits: ReadonlyArray<H>;\n /** Number of retrieval passes performed (≥ 1). */\n readonly iterations: number;\n /** Difficulty-gate verdict (whether the loop was eligible to run). */\n readonly gateHard: boolean;\n /** Final sufficiency verdict. */\n readonly sufficient: boolean;\n /**\n * `true` when the loop exhausted its cap / ran out of reformulations\n * while still judged insufficient — the caller should abstain rather\n * than answer from `hits`.\n */\n readonly abstained: boolean;\n /** The sequence of queries tried (original first). */\n readonly queries: ReadonlyArray<string>;\n}\n\n/**\n * Dependencies injected into {@link runIterativeRetrieval}. The loop does\n * no I/O of its own — `retrieve` and `grader` own all side effects.\n *\n * @stable\n */\nexport interface IterativeRetrievalDeps<H> {\n /**\n * Run one retrieval pass for `query`. `widen` is `true` on\n * reformulation passes so the caller can broaden recall (e.g. enable\n * P2-1 one-hop graph expansion).\n */\n retrieve(query: string, widen: boolean, signal?: AbortSignal): Promise<ReadonlyArray<H>>;\n /** Snippet shown to the grader for a hit. */\n snippetOf(hit: H): string;\n /** Stable id used to dedup hits across passes. */\n idOf(hit: H): string;\n /** Grader; `null` ⇒ single-shot (no grading, no provider call). */\n grader: RetrievalGrader | null;\n /**\n * Re-fuse the per-pass hit lists into one ranked list (MRET-2).\n * Receives one list per pass in pass order; the result feeds the\n * final `maxResults` cut so a pass-2 find can outrank pass-1 noise.\n * Absent ⇒ the loop falls back to round-robin interleaving (still\n * strictly better than the old discovery-order cut).\n */\n fuse?(lists: ReadonlyArray<ReadonlyArray<H>>): ReadonlyArray<H>;\n}\n\n/**\n * Options for {@link runIterativeRetrieval}.\n *\n * @stable\n */\nexport interface IterativeRetrievalOptions {\n /** Total-pass cap; clamped to `[1, {@link MAX_ITERATIONS_CEILING}]`. */\n readonly maxIterations?: number;\n /** Difficulty-gate tuning. */\n readonly difficulty?: DifficultyGateOptions;\n /** Skip the heuristic gate and force the loop (still capped). */\n readonly forceHard?: boolean;\n /** Max snippets passed to the grader per pass. Default 8. */\n readonly maxGradeSnippets?: number;\n /** Cap on the returned hit count (omitted ⇒ all accumulated). */\n readonly maxResults?: number;\n readonly signal?: AbortSignal;\n}\n\n/**\n * Run the gated grade-then-reformulate loop.\n *\n * Flow: assess difficulty → retrieve (pass 1) → if not hard *or* no\n * grader, return single-shot → else grade; if sufficient, return; if\n * weak and a reformulation is offered and the cap is not hit, retrieve\n * again (widened) and re-grade; otherwise **abstain**.\n *\n * @stable\n */\nexport async function runIterativeRetrieval<H>(\n query: string,\n deps: IterativeRetrievalDeps<H>,\n options: IterativeRetrievalOptions = {},\n): Promise<IterativeRetrievalResult<H>> {\n const gate =\n options.forceHard === true\n ? { hard: true, score: 1, signals: ['forced'] as ReadonlyArray<string> }\n : assessQueryDifficulty(query, options.difficulty ?? {});\n const cap = clampIterations(options.maxIterations);\n const maxSnippets = Math.max(1, options.maxGradeSnippets ?? DEFAULT_MAX_GRADE_SNIPPETS);\n const signal = options.signal;\n\n // MRET-2: hits are tracked PER PASS — RAW, including cross-pass\n // repeats, so rank fusion can reward consensus between passes. The\n // grade window and the final list dedup by id at read time. The old\n // flat append meant pass 1 saturated the first `maxSnippets` forever\n // and the final cut ran in discovery order.\n const passLists: Array<ReadonlyArray<H>> = [];\n const accumulate = (hits: ReadonlyArray<H>): void => {\n passLists.push([...hits]);\n };\n const queries: string[] = [query];\n const tried = new Set<string>([query.trim().toLowerCase()]);\n\n // Pass 1 — always single-shot, no widening.\n accumulate(await deps.retrieve(query, false, signal));\n\n // Gate: the loop is eligible only when the query is judged hard *and* a\n // grader exists. Otherwise stay single-shot — we did not grade, so we\n // make no abstention claim.\n const grader = deps.grader;\n if (!gate.hard || grader === null) {\n return finalize(deps, passLists, 1, gate.hard, true, false, queries, options.maxResults);\n }\n\n let passes = 1;\n for (;;) {\n // MRET-2: the grade window interleaves the TOP hits of every pass\n // (latest pass included) instead of replaying pass 1's head.\n // MRET-11: sufficiency is always judged against the ORIGINAL\n // question; reformulations ride along as already-tried context.\n const window = interleave(passLists, maxSnippets, deps.idOf).map(deps.snippetOf);\n const grade = await grader.grade(query, window, {\n ...(signal !== undefined ? { signal } : {}),\n ...(queries.length > 1 ? { triedQueries: queries.slice(1) } : {}),\n });\n if (grade.sufficient) {\n return finalize(deps, passLists, passes, true, true, false, queries, options.maxResults);\n }\n if (passes >= cap) break;\n const next = normalizeReformulation(grade.reformulation, tried);\n if (next === null) break;\n passes += 1;\n queries.push(next);\n tried.add(next.toLowerCase());\n accumulate(await deps.retrieve(next, true, signal));\n }\n // Cap reached or no further reformulation while still insufficient ⇒ abstain.\n return finalize(deps, passLists, passes, true, false, true, queries, options.maxResults);\n}\n\n/**\n * Round-robin interleave of per-pass lists, deduped by id: hit 1 of\n * every pass, then hit 2 of every pass, … — guarantees the latest\n * pass's top hits enter a window of any size.\n */\nfunction interleave<H>(\n lists: ReadonlyArray<ReadonlyArray<H>>,\n limit: number,\n idOf: (hit: H) => string,\n): H[] {\n const out: H[] = [];\n const seen = new Set<string>();\n const longest = lists.reduce((max, l) => Math.max(max, l.length), 0);\n for (let rank = 0; rank < longest && out.length < limit; rank += 1) {\n for (const list of lists) {\n const hit = list[rank];\n if (hit === undefined) continue;\n const id = idOf(hit);\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(hit);\n if (out.length >= limit) break;\n }\n }\n return out;\n}\n\nfunction finalize<H>(\n deps: IterativeRetrievalDeps<H>,\n passLists: ReadonlyArray<ReadonlyArray<H>>,\n iterations: number,\n gateHard: boolean,\n sufficient: boolean,\n abstained: boolean,\n queries: ReadonlyArray<string>,\n maxResults: number | undefined,\n): IterativeRetrievalResult<H> {\n // MRET-2: re-rank ACROSS passes before the cut — discovery order let\n // pass-1 noise crowd out the pass-2 hit that actually answers.\n const ranked =\n typeof deps.fuse === 'function'\n ? dedupBy(deps.fuse(passLists), deps.idOf)\n : interleave(passLists, Number.POSITIVE_INFINITY, deps.idOf);\n const capped =\n maxResults !== undefined && maxResults >= 0 ? ranked.slice(0, maxResults) : ranked.slice();\n return { hits: capped, iterations, gateHard, sufficient, abstained, queries: [...queries] };\n}\n\nfunction dedupBy<H>(hits: ReadonlyArray<H>, idOf: (hit: H) => string): H[] {\n const seen = new Set<string>();\n const out: H[] = [];\n for (const hit of hits) {\n const id = idOf(hit);\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(hit);\n }\n return out;\n}\n\nfunction clampIterations(value: number | undefined): number {\n const n = value ?? DEFAULT_MAX_ITERATIONS;\n if (!Number.isFinite(n)) return DEFAULT_MAX_ITERATIONS;\n return Math.min(MAX_ITERATIONS_CEILING, Math.max(1, Math.floor(n)));\n}\n\n/**\n * Normalise a proposed reformulation: trim, drop empties, and reject a\n * query already tried (case-insensitive) so the loop never re-treads and\n * cannot spin.\n */\nfunction normalizeReformulation(raw: string | null, tried: ReadonlySet<string>): string | null {\n if (raw === null) return null;\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n if (tried.has(trimmed.toLowerCase())) return null;\n return trimmed;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction wordCount(text: string): number {\n const trimmed = text.trim();\n return trimmed.length === 0 ? 0 : trimmed.split(/\\s+/u).length;\n}\n\nfunction countCommas(text: string): number {\n return (text.match(/,/gu) ?? []).length;\n}\n\nfunction questionMarkCount(text: string): number {\n return (text.match(/\\?/gu) ?? []).length;\n}\n\nfunction coerceBool(value: unknown): boolean | null {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n const v = value.trim().toLowerCase();\n if (v === 'true' || v === 'yes' || v === 'sufficient') return true;\n if (v === 'false' || v === 'no' || v === 'insufficient') return false;\n }\n return null;\n}\n\nfunction coerceConfidence(value: unknown, sufficient: boolean): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return Math.min(1, Math.max(0, value));\n }\n return sufficient ? 0.8 : 0.3;\n}\n\nfunction coerceReformulation(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n return trimmed.length > 1024 ? trimmed.slice(0, 1024) : trimmed;\n}\n\nfunction coerceReason(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return trimmed.length === 0 ? undefined : trimmed;\n}\n\nfunction tryParseObject(text: string): Record<string, unknown> | null {\n const direct = tryJson(text);\n if (isRecord(direct)) return direct;\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start >= 0 && end > start) {\n const sliced = tryJson(text.slice(start, end + 1));\n if (isRecord(sliced)) return sliced;\n }\n return null;\n}\n\nfunction tryJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n"],"mappings":";;AAqCA,MAAa,yBAAyB;;;;;;AAOtC,MAAa,yBAAyB;;AAGtC,MAAa,+BAA+B;;AAG5C,MAAM,2BAA2B;;AAGjC,MAAM,6BAA6B;;AAgCnC,MAAM,iBAAiB;AACvB,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,cACJ;;AAEF,MAAM,cACJ;;AAEF,MAAM,gBACJ;;AAEF,MAAM,iBAAiB;AAEvB,MAAM,cAAc;AACpB,MAAM,aAAa;AACnB,MAAM,eAAe;AACrB,MAAM,iBAAiB;AACvB,MAAM,SAAS;AACf,MAAM,kBAAkB;;;;;;;;;;;AAYxB,SAAgB,sBACd,OACA,UAAiC,EAAE,EACb;CACtB,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAMA,UAAoB,EAAE;CAC5B,IAAI,QAAQ;CAEZ,MAAM,mBAAmB,MAAM,MAAM,cAAc,IAAI,EAAE,EAAE;AAM3D,KAJE,eAAe,KAAK,MAAM,IAC1B,YAAY,KAAK,MAAM,IACvB,YAAY,KAAK,MAAM,IACvB,mBAAmB,GACP;AACZ,UAAQ,KAAK,YAAY;AACzB,WAAS;;AAEX,KAAI,YAAY,KAAK,MAAM,EAAE;AAC3B,UAAQ,KAAK,WAAW;AACxB,WAAS;;AAEX,KAAI,cAAc,KAAK,MAAM,EAAE;AAC7B,UAAQ,KAAK,aAAa;AAC1B,WAAS;;AAGX,MADuB,MAAM,MAAM,eAAe,IAAI,EAAE,EAAE,SAAS,YAAY,MAAM,IAChE,KAAK,kBAAkB,MAAM,IAAI,GAAG;AACvD,UAAQ,KAAK,eAAe;AAC5B,WAAS;;AAEX,KAAI,UAAU,MAAM,IAAI,iBAAiB;AACvC,UAAQ,KAAK,OAAO;AACpB,WAAS;;CAGX,MAAM,UAAU,KAAK,IAAI,GAAG,MAAM;AAClC,QAAO;EAAE,MAAM,WAAW;EAAW,OAAO;EAAS;EAAS;;;;;;;;AAqEhE,MAAa,gCACX;;;;;;;AAgBF,SAAgB,kBACd,OACA,UACA,UAII,EAAE,EACW;AAWjB,QAAO;EACL,UAAU,CAAC;GAAE,MAAM;GAAQ,SAJ3B,aAAa,MAAM,2BANnB,SAAS,WAAW,IAAI,WAAW,SAAS,KAAK,GAAG,MAAM,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,KAAK,GAEvF,QAAQ,iBAAiB,UAAa,QAAQ,aAAa,SAAS,IAChE,mDAAmD,QAAQ,aAAa,KAAK,MAAM,KACnF,GAEuD;GAIvB,CAAC;EACrC,eAAe;EACf,aAAa;EACb,WAAW,QAAQ,aAAa;EAChC,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,YAAY,EAAE,MAAM,cAAc;EACnC;;;;;;;;;;;;;;;AAgBH,SAAgB,WAAW,MAA0C;CACnE,MAAMC,OAAuB;EAAE,YAAY;EAAM,YAAY;EAAG,eAAe;EAAM;AACrF,KAAI,SAAS,OAAW,QAAO;CAC/B,MAAM,WAAW,WAAW,KAAK,CAAC,MAAM;AACxC,KAAI,SAAS,WAAW,EAAG,QAAO;CAClC,MAAM,MAAM,eAAe,SAAS;AACpC,KAAI,QAAQ,KAAM,QAAO;CACzB,MAAM,aAAa,WAAW,IAAI,WAAW;AAC7C,KAAI,eAAe,KAAM,QAAO;CAChC,MAAM,aAAa,iBAAiB,IAAI,YAAY,WAAW;CAC/D,MAAM,gBAAgB,aAAa,OAAO,oBAAoB,IAAI,cAAc;CAChF,MAAM,SAAS,aAAa,IAAI,OAAO;AACvC,QAAO;EACL;EACA;EACA;EACA,GAAI,WAAW,SAAY,EAAE,QAAQ,GAAG,EAAE;EAC3C;;;;;;;;;;AAWH,SAAgB,8BACd,UACA,UAA2C,EAAE,EAC5B;CACjB,MAAM,YAAY,QAAQ,aAAa;AACvC,QAAO,EACL,MAAM,MAAM,OAAO,UAAU,OAAO,EAAE,EAA2B;AAC/D,MAAI;GACF,MAAM,UAAU,kBAAkB,OAAO,UAAU;IACjD;IACA,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,QAAQ,GAAG,EAAE;IAC5D,GAAI,KAAK,iBAAiB,SAAY,EAAE,cAAc,KAAK,cAAc,GAAG,EAAE;IAC/E,CAAC;AAEF,UAAO,YADU,MAAM,SAAS,SAAS,QAAQ,EACtB,KAAK;UAC1B;AACN,UAAO;IAAE,YAAY;IAAM,YAAY;IAAG,eAAe;IAAM;;IAGpE;;;;;;;;;;;;AAyFH,eAAsB,sBACpB,OACA,MACA,UAAqC,EAAE,EACD;CACtC,MAAM,OACJ,QAAQ,cAAc,OAClB;EAAE,MAAM;EAAM,OAAO;EAAG,SAAS,CAAC,SAAS;EAA2B,GACtE,sBAAsB,OAAO,QAAQ,cAAc,EAAE,CAAC;CAC5D,MAAM,MAAM,gBAAgB,QAAQ,cAAc;CAClD,MAAM,cAAc,KAAK,IAAI,GAAG,QAAQ,oBAAoB,2BAA2B;CACvF,MAAM,SAAS,QAAQ;CAOvB,MAAMC,YAAqC,EAAE;CAC7C,MAAM,cAAc,SAAiC;AACnD,YAAU,KAAK,CAAC,GAAG,KAAK,CAAC;;CAE3B,MAAMC,UAAoB,CAAC,MAAM;CACjC,MAAM,QAAQ,IAAI,IAAY,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;AAG3D,YAAW,MAAM,KAAK,SAAS,OAAO,OAAO,OAAO,CAAC;CAKrD,MAAM,SAAS,KAAK;AACpB,KAAI,CAAC,KAAK,QAAQ,WAAW,KAC3B,QAAO,SAAS,MAAM,WAAW,GAAG,KAAK,MAAM,MAAM,OAAO,SAAS,QAAQ,WAAW;CAG1F,IAAI,SAAS;AACb,UAAS;EAKP,MAAM,SAAS,WAAW,WAAW,aAAa,KAAK,KAAK,CAAC,IAAI,KAAK,UAAU;EAChF,MAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,QAAQ;GAC9C,GAAI,WAAW,SAAY,EAAE,QAAQ,GAAG,EAAE;GAC1C,GAAI,QAAQ,SAAS,IAAI,EAAE,cAAc,QAAQ,MAAM,EAAE,EAAE,GAAG,EAAE;GACjE,CAAC;AACF,MAAI,MAAM,WACR,QAAO,SAAS,MAAM,WAAW,QAAQ,MAAM,MAAM,OAAO,SAAS,QAAQ,WAAW;AAE1F,MAAI,UAAU,IAAK;EACnB,MAAM,OAAO,uBAAuB,MAAM,eAAe,MAAM;AAC/D,MAAI,SAAS,KAAM;AACnB,YAAU;AACV,UAAQ,KAAK,KAAK;AAClB,QAAM,IAAI,KAAK,aAAa,CAAC;AAC7B,aAAW,MAAM,KAAK,SAAS,MAAM,MAAM,OAAO,CAAC;;AAGrD,QAAO,SAAS,MAAM,WAAW,QAAQ,MAAM,OAAO,MAAM,SAAS,QAAQ,WAAW;;;;;;;AAQ1F,SAAS,WACP,OACA,OACA,MACK;CACL,MAAMC,MAAW,EAAE;CACnB,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,UAAU,MAAM,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,OAAO,EAAE,EAAE;AACpE,MAAK,IAAI,OAAO,GAAG,OAAO,WAAW,IAAI,SAAS,OAAO,QAAQ,EAC/D,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,OAAW;EACvB,MAAM,KAAK,KAAK,IAAI;AACpB,MAAI,KAAK,IAAI,GAAG,CAAE;AAClB,OAAK,IAAI,GAAG;AACZ,MAAI,KAAK,IAAI;AACb,MAAI,IAAI,UAAU,MAAO;;AAG7B,QAAO;;AAGT,SAAS,SACP,MACA,WACA,YACA,UACA,YACA,WACA,SACA,YAC6B;CAG7B,MAAM,SACJ,OAAO,KAAK,SAAS,aACjB,QAAQ,KAAK,KAAK,UAAU,EAAE,KAAK,KAAK,GACxC,WAAW,WAAW,OAAO,mBAAmB,KAAK,KAAK;AAGhE,QAAO;EAAE,MADP,eAAe,UAAa,cAAc,IAAI,OAAO,MAAM,GAAG,WAAW,GAAG,OAAO,OAAO;EACrE;EAAY;EAAU;EAAY;EAAW,SAAS,CAAC,GAAG,QAAQ;EAAE;;AAG7F,SAAS,QAAW,MAAwB,MAA+B;CACzE,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAMA,MAAW,EAAE;AACnB,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,KAAK,KAAK,IAAI;AACpB,MAAI,KAAK,IAAI,GAAG,CAAE;AAClB,OAAK,IAAI,GAAG;AACZ,MAAI,KAAK,IAAI;;AAEf,QAAO;;AAGT,SAAS,gBAAgB,OAAmC;CAC1D,MAAM,IAAI,SAAS;AACnB,KAAI,CAAC,OAAO,SAAS,EAAE,CAAE,QAAO;AAChC,QAAO,KAAK,IAAI,wBAAwB,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC,CAAC;;;;;;;AAQrE,SAAS,uBAAuB,KAAoB,OAA2C;AAC7F,KAAI,QAAQ,KAAM,QAAO;CACzB,MAAM,UAAU,IAAI,MAAM;AAC1B,KAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,KAAI,MAAM,IAAI,QAAQ,aAAa,CAAC,CAAE,QAAO;AAC7C,QAAO;;AAOT,SAAS,UAAU,MAAsB;CACvC,MAAM,UAAU,KAAK,MAAM;AAC3B,QAAO,QAAQ,WAAW,IAAI,IAAI,QAAQ,MAAM,OAAO,CAAC;;AAG1D,SAAS,YAAY,MAAsB;AACzC,SAAQ,KAAK,MAAM,MAAM,IAAI,EAAE,EAAE;;AAGnC,SAAS,kBAAkB,MAAsB;AAC/C,SAAQ,KAAK,MAAM,OAAO,IAAI,EAAE,EAAE;;AAGpC,SAAS,WAAW,OAAgC;AAClD,KAAI,OAAO,UAAU,UAAW,QAAO;AACvC,KAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,IAAI,MAAM,MAAM,CAAC,aAAa;AACpC,MAAI,MAAM,UAAU,MAAM,SAAS,MAAM,aAAc,QAAO;AAC9D,MAAI,MAAM,WAAW,MAAM,QAAQ,MAAM,eAAgB,QAAO;;AAElE,QAAO;;AAGT,SAAS,iBAAiB,OAAgB,YAA6B;AACrE,KAAI,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,CACrD,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC;AAExC,QAAO,aAAa,KAAM;;AAG5B,SAAS,oBAAoB,OAA+B;AAC1D,KAAI,OAAO,UAAU,SAAU,QAAO;CACtC,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAO,QAAQ,SAAS,OAAO,QAAQ,MAAM,GAAG,KAAK,GAAG;;AAG1D,SAAS,aAAa,OAAoC;AACxD,KAAI,OAAO,UAAU,SAAU,QAAO;CACtC,MAAM,UAAU,MAAM,MAAM;AAC5B,QAAO,QAAQ,WAAW,IAAI,SAAY;;AAG5C,SAAS,eAAe,MAA8C;CACpE,MAAM,SAAS,QAAQ,KAAK;AAC5B,KAAI,SAAS,OAAO,CAAE,QAAO;CAC7B,MAAM,QAAQ,KAAK,QAAQ,IAAI;CAC/B,MAAM,MAAM,KAAK,YAAY,IAAI;AACjC,KAAI,SAAS,KAAK,MAAM,OAAO;EAC7B,MAAM,SAAS,QAAQ,KAAK,MAAM,OAAO,MAAM,EAAE,CAAC;AAClD,MAAI,SAAS,OAAO,CAAE,QAAO;;AAE/B,QAAO;;AAGT,SAAS,QAAQ,MAAuB;AACtC,KAAI;AACF,SAAO,KAAK,MAAM,KAAK;SACjB;AACN;;;AAIJ,SAAS,WAAW,MAAsB;AAExC,QADc,+BAA+B,KAAK,KAAK,MAAM,CAAC,GAC/C,MAAM;;AAGvB,SAAS,SAAS,OAAkD;AAClE,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM"}
|