@lunora/ai 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
@@ -0,0 +1,109 @@
1
+ import { EmbeddingModel, LanguageModel } from 'ai';
2
+ /**
3
+ * Structural projection of the Cloudflare Workers `AI` binding (`env.AI`).
4
+ * Declared locally so unit tests can pass a plain-object double and the real
5
+ * binding satisfies the same shape without importing `@cloudflare/workers-types`
6
+ * into the public surface. Mirrors the `run` method documented at
7
+ * https://developers.cloudflare.com/workers-ai/.
8
+ * @experimental
9
+ */
10
+ interface AiBindingLike {
11
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
12
+ }
13
+ /**
14
+ * A Workers AI provider instance — the value returned by `createWorkersAI(...)`.
15
+ * Calling it with a model id yields an AI SDK {@link LanguageModel}; the
16
+ * optional `textEmbeddingModel` factory yields an {@link EmbeddingModel}.
17
+ * Typed structurally so `@lunora/ai` neither re-declares the provider's full
18
+ * surface nor hard-pins its exact type across minor releases.
19
+ * @experimental
20
+ */
21
+ interface WorkersAiProviderLike {
22
+ (modelId: string, settings?: Record<string, unknown>): LanguageModel;
23
+ textEmbeddingModel?: (modelId: string) => EmbeddingModel;
24
+ }
25
+ /**
26
+ * AI Gateway options forwarded to `createWorkersAI`. Lets inference route
27
+ * through a Cloudflare AI Gateway for caching, rate-limiting, and observability.
28
+ * @experimental
29
+ */
30
+ interface AiGatewayOptions {
31
+ [key: string]: unknown;
32
+ id: string;
33
+ }
34
+ /**
35
+ * `LunoraAiOptions` is part of the experimental `@lunora/ai` API and may change without a major version bump.
36
+ * @experimental
37
+ */
38
+ interface LunoraAiOptions {
39
+ /**
40
+ * The Workers `AI` binding (`env.AI`). Required for the zero-config Workers
41
+ * AI default and for the raw `ai.run(...)` passthrough. May be omitted when
42
+ * a pre-built `provider` is supplied (e.g. in tests or a custom setup).
43
+ */
44
+ binding?: AiBindingLike;
45
+ /**
46
+ * Default Workers AI **embedding** model id used by `embeddingModel()` when no
47
+ * explicit model is passed (e.g. `@cf/baai/bge-base-en-v1.5`). Kept separate
48
+ * from `defaultModel` because a language-model id and an embedding-model
49
+ * id belong to different Workers AI families and are never interchangeable —
50
+ * reusing the language-model default here would defer a wrong-family error to
51
+ * inference time. Has no effect on bring-your-own providers.
52
+ */
53
+ defaultEmbeddingModel?: string;
54
+ /**
55
+ * Default Workers AI **language** model id used by `model()` when no explicit
56
+ * model is passed. For embeddings, set `defaultEmbeddingModel` instead.
57
+ * Has no effect on bring-your-own providers.
58
+ */
59
+ defaultModel?: string;
60
+ /** Route Workers AI inference through a Cloudflare AI Gateway. */
61
+ gateway?: AiGatewayOptions;
62
+ /**
63
+ * Pre-built Workers AI provider. When omitted, one is constructed from
64
+ * `binding` via `createWorkersAI`. Supplying it directly is the seam used by
65
+ * tests and advanced setups; it also lets callers configure the provider
66
+ * (e.g. `safePrompt`) before handing it to `@lunora/ai`.
67
+ */
68
+ provider?: WorkersAiProviderLike;
69
+ }
70
+ /**
71
+ * A model to run against. The AI SDK's {@link LanguageModel} already admits a
72
+ * bare `string`, so this alias covers both arms of the provider-agnostic seam:
73
+ * a string id is the Workers AI convenience path (resolved by `ctx.ai.model`),
74
+ * a built model object is bring-your-own (`@ai-sdk/openai`, `@ai-sdk/anthropic`,
75
+ * `@ai-sdk/google`, OpenRouter, …).
76
+ * @experimental
77
+ */
78
+ type ModelInput = LanguageModel;
79
+ /**
80
+ * Likewise for embeddings: a Workers AI embedding model id (e.g.
81
+ * `@cf/baai/bge-base-en-v1.5`) or any AI SDK {@link EmbeddingModel}.
82
+ * @experimental
83
+ */
84
+ type EmbeddingModelInput = EmbeddingModel | string;
85
+ /**
86
+ * The `ctx.ai` surface. `model`/`embeddingModel` resolve a Workers AI model from
87
+ * a string (the default provider) and pass any non-string model straight through,
88
+ * so both accept Workers AI and bring-your-own providers. Feed the resolved model
89
+ * to the AI SDK functions re-exported from `@lunora/ai` (`generateText`,
90
+ * `streamText`, `generateObject`, `embed`, …); `run` is the raw binding escape
91
+ * hatch, and `workersai` is the underlying provider for direct model access.
92
+ * @experimental
93
+ */
94
+ interface LunoraAi {
95
+ /** Resolve an {@link EmbeddingModel}: a string → Workers AI, an object → passthrough. */
96
+ embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
97
+ /** Resolve a {@link LanguageModel}: a string → Workers AI, an object → passthrough. */
98
+ model: (model?: ModelInput) => LanguageModel;
99
+ /**
100
+ * Raw Workers AI binding passthrough (void-style `ai.run`). Bypasses the AI
101
+ * SDK entirely — useful for Workers-AI-only model families (image, ASR,
102
+ * translation) not surfaced through the provider. Throws if no binding was
103
+ * supplied.
104
+ */
105
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
106
+ /** The underlying Workers AI provider — `ai.workersai("@cf/...")` for a raw model. */
107
+ workersai: WorkersAiProviderLike;
108
+ }
109
+ export { AiBindingLike as A, EmbeddingModelInput as E, LunoraAiOptions as L, ModelInput as M, WorkersAiProviderLike as W, LunoraAi as a, AiGatewayOptions as b };
@@ -0,0 +1,550 @@
1
+ import { Tool } from 'ai';
2
+ import { E as EmbeddingModelInput, a as LunoraAi } from "../packem_shared/types.d-BXCiRv1x.mjs";
3
+ /**
4
+ * Built-in fixed-window chunker: split into `size`-char windows overlapping by
5
+ * `overlap` chars. Deliberately simple and deterministic — the zero-config
6
+ * default. Token-aware / sentence / semantic strategies plug in via
7
+ * `RagConfig.chunk`.
8
+ * @experimental
9
+ */
10
+ declare const fixedWindowChunks: (text: string, size: number, overlap: number) => ReadonlyArray<string>;
11
+ /**
12
+ * `(text) => vector` — the embedder shape `ctx.vectors` accepts on both its
13
+ * write (`upsert`) and read (`query`) inputs. Matches `@lunora/server`'s
14
+ * `VectorEmbedder` and `@lunora/bindings/vectors`' `EmbedFunction&lt;string>`.
15
+ * @experimental
16
+ */
17
+ type RagEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
18
+ /**
19
+ * `RagVectorMatch` is part of the experimental `@lunora/ai` API and may change without a major version bump.
20
+ * @experimental
21
+ */
22
+ interface RagVectorMatch {
23
+ id: string;
24
+ metadata?: Record<string, unknown>;
25
+ score: number;
26
+ }
27
+ /**
28
+ * `RagVectorMatches` is part of the experimental `@lunora/ai` API and may change without a major version bump.
29
+ * @experimental
30
+ */
31
+ interface RagVectorMatches {
32
+ count: number;
33
+ matches: ReadonlyArray<RagVectorMatch>;
34
+ }
35
+ /**
36
+ * `RagVectorQueryInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
37
+ * @experimental
38
+ */
39
+ interface RagVectorQueryInput {
40
+ /** Embedder used to vectorize `input`. */
41
+ embed?: RagEmbedder;
42
+ filter?: Record<string, unknown>;
43
+ /** Natural-language query text, embedded via `embed`. */
44
+ input?: string;
45
+ namespace?: string;
46
+ /**
47
+ * How much stored metadata to return on matches. The runtime honours it even
48
+ * though `@lunora/server`'s ctx type does not declare it — the helper relies
49
+ * on it to read chunk text back in metadata mode.
50
+ */
51
+ returnMetadata?: "all" | "indexed" | "none";
52
+ topK?: number;
53
+ }
54
+ /**
55
+ * `RagVectorRecord` is part of the experimental `@lunora/ai` API and may change without a major version bump.
56
+ * @experimental
57
+ */
58
+ interface RagVectorRecord {
59
+ id: string;
60
+ metadata?: Record<string, unknown>;
61
+ }
62
+ /**
63
+ * `RagVectorUpsertInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
64
+ * @experimental
65
+ */
66
+ interface RagVectorUpsertInput {
67
+ /** Embedder used to vectorize `input`. Optional — omitted for text-search indexes. */
68
+ embed?: RagEmbedder;
69
+ id: string;
70
+ input: string;
71
+ metadata?: Record<string, unknown>;
72
+ namespace?: string;
73
+ }
74
+ /**
75
+ * Structural subset of the vector surface the RAG helper needs. Both the
76
+ * `ctx.vectors` facade on Mutation/Action ctx (`@lunora/server`'s
77
+ * `VectorSearch`) and the raw `@lunora/bindings/vectors` `LunoraVectors`
78
+ * satisfy it — declared here so `@lunora/ai` depends on neither package.
79
+ * @experimental
80
+ */
81
+ interface RagVectors {
82
+ deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<unknown>;
83
+ getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
84
+ query: (indexName: string, input: RagVectorQueryInput) => Promise<RagVectorMatches>;
85
+ upsert: (indexName: string, input: RagVectorUpsertInput) => Promise<unknown>;
86
+ }
87
+ /**
88
+ * The two facades `defineRag` binds. An `ActionCtx` satisfies this directly
89
+ * (`ctx.ai` is action-only, so RAG methods run inside actions); any object
90
+ * carrying the two facades works in tests.
91
+ * @experimental
92
+ */
93
+ interface RagContext {
94
+ /**
95
+ * Resolves a Workers AI embedding-model id (or the omitted default) — an
96
+ * `ActionCtx`'s `ctx.ai` satisfies it. OPTIONAL: when
97
+ * {@link RagConfig.embeddingModel} is a direct AI SDK `EmbeddingModel` object
98
+ * (bring-your-own embeddings, e.g. `@ai-sdk/openai`), the helper uses that
99
+ * object as-is and never reads `ai`, so a hand-built context may omit it and
100
+ * no `env.AI` binding is needed. A model-id string (or an omitted model) with
101
+ * no `ai` present throws a directed error.
102
+ */
103
+ ai?: Pick<LunoraAi, "embeddingModel">;
104
+ /**
105
+ * The verified retrieval identity, read by {@link RagConfig.rlsFilter} to
106
+ * derive a per-request row filter. An `ActionCtx` carrying `ctx.auth`
107
+ * satisfies this structurally, so `docs(ctx)` picks the identity up
108
+ * automatically; tests pass any value. `unknown` on purpose — `@lunora/ai`
109
+ * stays decoupled from `@lunora/server`'s identity type; `rlsFilter` narrows.
110
+ */
111
+ auth?: unknown;
112
+ /**
113
+ * Optional `ctx.trace` span factory — an `ActionCtx`'s `ctx.trace` satisfies
114
+ * it structurally. When present, `defineRag` wraps each embedding-model
115
+ * call in a `generation` span carrying `gen_ai.operation.name: "embeddings"`
116
+ * and `gen_ai.request.model` up front, plus — attached post-hoc through the
117
+ * span handle the tracer hands the body — `gen_ai.usage.input_tokens` (from
118
+ * the embed result's token usage) and `gen_ai.usage.cost` (probed from the
119
+ * embed result's provider metadata, e.g. AI Gateway) when those are present.
120
+ * So the embed shows up on the trace waterfall with its usage like any other
121
+ * instrumented model call. `unknown` on purpose — the same decoupling
122
+ * rationale as `auth`: `defineRag` narrows it to a callable and runs embeds
123
+ * untraced when it is absent (a hand-built context / test).
124
+ */
125
+ trace?: unknown;
126
+ vectors: RagVectors;
127
+ }
128
+ /**
129
+ * Pluggable chunk-text storage. By default chunk text is stored in vector
130
+ * metadata (`__ragText`), which forces `returnMetadata: "all"` on retrieval and
131
+ * caps `topK` at 20 (the Vectorize full-metadata ceiling) — and each vector's
132
+ * metadata must stay under the ~10 KiB Vectorize cap. Supplying a text store
133
+ * (a DO table, KV, …) moves the text out of metadata: retrieval queries with
134
+ * `returnMetadata: "indexed"` (topK up to 100) and hydrates text by chunk id.
135
+ * @experimental
136
+ */
137
+ interface RagTextStore {
138
+ /** Fetch chunk texts by id, aligned with the input order; `undefined` for misses. */
139
+ getMany: (ids: ReadonlyArray<string>, options: {
140
+ namespace?: string;
141
+ }) => Promise<ReadonlyArray<string | undefined>>;
142
+ /** Persist chunk texts. Must be idempotent by chunk `id` (re-index re-puts). */
143
+ put: (chunks: ReadonlyArray<StoredRagChunk>, options: {
144
+ namespace?: string;
145
+ }) => Promise<void>;
146
+ /** Optional cleanup hook, invoked when a source's chunks are deleted. */
147
+ remove?: (ids: ReadonlyArray<string>, options: {
148
+ namespace?: string;
149
+ }) => Promise<void>;
150
+ }
151
+ /**
152
+ * A chunk handed to {@link RagTextStore.put} / {@link RagLexicalStore.index}.
153
+ * @experimental
154
+ */
155
+ interface StoredRagChunk {
156
+ chunkIndex: number;
157
+ id: string;
158
+ sourceId: string;
159
+ text: string;
160
+ }
161
+ /**
162
+ * One lexical (BM25) hit returned by {@link RagLexicalStore.search}.
163
+ * @experimental
164
+ */
165
+ interface LexicalMatch {
166
+ /** The chunk vector id — the same id scheme the vector leg uses, so RRF can fuse the two. */
167
+ id: string;
168
+ /** BM25 relevance score (higher = better). Used only for the leg's internal ranking; RRF fuses by rank. */
169
+ score: number;
170
+ /** The chunk text, returned so a fused lexical-only hit needs no extra hydration round-trip. */
171
+ text: string;
172
+ }
173
+ /**
174
+ * Pluggable lexical (BM25 / keyword) store — the production seam for hybrid
175
+ * retrieval. When {@link RagConfig.lexicalStore} is set, `index()` mirrors each
176
+ * chunk's text here and `retrieve()` fuses this store's keyword ranking with the
177
+ * vector store's semantic ranking via Reciprocal Rank Fusion. Mirrors the
178
+ * {@link RagTextStore} shape (idempotent by chunk `id`, namespace-partitioned).
179
+ *
180
+ * `@lunora/ai/rag` ships `bm25LexicalStore()`, an in-memory reference adapter;
181
+ * production deployments plug a durable one (DO SQLite inverted index, D1,
182
+ * Vectorize-adjacent search service, …) behind this same interface.
183
+ * @experimental
184
+ */
185
+ interface RagLexicalStore {
186
+ /** Index chunk texts for keyword search. Must be idempotent by chunk `id` (re-index re-puts). */
187
+ index: (chunks: ReadonlyArray<StoredRagChunk>, options: {
188
+ namespace?: string;
189
+ }) => Promise<void>;
190
+ /** Optional cleanup hook, invoked when a source's chunks are deleted or a re-index shrinks it. */
191
+ remove?: (ids: ReadonlyArray<string>, options: {
192
+ namespace?: string;
193
+ }) => Promise<void>;
194
+ /**
195
+ * Rank chunks by lexical relevance to `query`. `filter` carries the same
196
+ * (RLS-merged) metadata predicate handed to the vector leg — a store that
197
+ * indexes metadata MUST honour it so hybrid retrieval can't surface a row
198
+ * the RLS filter would exclude; a namespace-only store (the reference
199
+ * adapter) isolates by `namespace` and documents that it ignores `filter`.
200
+ */
201
+ search: (query: string, options: {
202
+ filter?: Record<string, unknown>;
203
+ namespace?: string;
204
+ topK: number;
205
+ }) => Promise<ReadonlyArray<LexicalMatch>>;
206
+ }
207
+ /**
208
+ * A pre-defined, reusable filter expression. Declared on `RagConfig.filters`
209
+ * (keyed by name) and referenced by name from `RetrieveOptions.filter` — avoids
210
+ * repeating the same tenant/RBAC filter shape across every retrieval site.
211
+ * @example
212
+ * ```ts
213
+ * const docs = defineRag({
214
+ * index: "docs",
215
+ * filters: {
216
+ * published: { filter: { status: "published", deleted: false }, description: "Only published content" },
217
+ * },
218
+ * });
219
+ * // Later — reference by name:
220
+ * docs(ctx).retrieve("query", { filter: "published" });
221
+ * ```
222
+ * @experimental
223
+ */
224
+ interface RagNamedFilter {
225
+ /** Optional human-readable description for observability / Studio display. */
226
+ description?: string;
227
+ /** The filter expression passed verbatim to Vectorize's `filter` parameter. */
228
+ filter: Record<string, unknown>;
229
+ }
230
+ /**
231
+ * `RagConfig` is part of the experimental `@lunora/ai` API and may change without a major version bump.
232
+ * @experimental
233
+ */
234
+ interface RagConfig {
235
+ /**
236
+ * Suppress the one-time dev warning emitted when `index`/`retrieve` run
237
+ * without a `namespace`. Only appropriate for genuinely single-tenant apps —
238
+ * Vectorize indexes are account-global, so a namespace-less index shares
239
+ * vectors across every tenant.
240
+ */
241
+ allowSharedNamespace?: boolean;
242
+ /** Custom chunker; overrides the built-in fixed-window splitter. */
243
+ chunk?: (text: string) => ReadonlyArray<string>;
244
+ /** Overlap (chars) between adjacent chunks. Default 200. Must be < `chunkSize`. */
245
+ chunkOverlap?: number;
246
+ /** Target chunk size (chars). Default 1000. */
247
+ chunkSize?: number;
248
+ /**
249
+ * Embedding model, declared once so index + retrieve embed identically: a
250
+ * Workers AI id (e.g. `@cf/baai/bge-base-en-v1.5`) or any AI SDK
251
+ * `EmbeddingModel`. Falls back to `createAi`'s `defaultModel` when omitted.
252
+ */
253
+ embeddingModel?: EmbeddingModelInput;
254
+ /**
255
+ * Embedding-model version tag — an opt-in discriminator that partitions the
256
+ * vector space so a model swap can never silently return garbage. Vectors
257
+ * embedded by one model live in a different space from another's, and
258
+ * querying across the two returns meaningless neighbours. When set, the tag
259
+ * is folded into the effective Vectorize namespace (and chunk-id prefix) of
260
+ * every index/retrieve/remove, so bumping it re-partitions cleanly: old
261
+ * vectors become unreachable to new queries (empty ≫ wrong) until sources
262
+ * are re-indexed under the new tag.
263
+ *
264
+ * Set + bump this whenever you change {@link RagConfig.embeddingModel} (or
265
+ * its dimensions). Opt-in and non-breaking — omitting it keeps the exact
266
+ * chunk-id/namespace scheme of un-versioned indexes. Must match
267
+ * `^[A-Za-z0-9._-]{1,40}$` (e.g. `"bge-v1.5"`, `"v2"`).
268
+ */
269
+ embeddingModelVersion?: string;
270
+ /**
271
+ * Pre-defined named filter expressions. Each key is a filter name users
272
+ * pass through `RetrieveOptions.filter`. Throws at retrieve-time if the
273
+ * name is not found here — catches spelling mistakes early.
274
+ */
275
+ filters?: Record<string, RagNamedFilter>;
276
+ /** The Vectorize index name (a `ctx.vectors` index binding key). */
277
+ index: string;
278
+ /**
279
+ * Pluggable lexical (BM25) store for hybrid retrieval. When set, `index()`
280
+ * mirrors chunk text into it and `retrieve()` fuses the vector (semantic)
281
+ * and lexical (keyword) rankings via Reciprocal Rank Fusion — recovering the
282
+ * exact-term / rare-token matches a pure-embedding search misses. Use the
283
+ * shipped `bm25LexicalStore()` reference adapter or plug your own durable
284
+ * one. See {@link RagLexicalStore}.
285
+ */
286
+ lexicalStore?: RagLexicalStore;
287
+ /** Retrieval depth for the lexical leg of hybrid search. Defaults to the effective `topK`. */
288
+ lexicalTopK?: number;
289
+ /**
290
+ * Enforce tenant isolation: throw (instead of the one-time dev warning)
291
+ * when `index`/`retrieve`/`remove` run without a `namespace`. Recommended
292
+ * for every multi-tenant app — Vectorize indexes are account-global, and
293
+ * in metadata mode the leaked payload includes raw chunk text.
294
+ */
295
+ requireNamespace?: boolean;
296
+ /**
297
+ * Row-level-security filter derived from the retrieval identity. Called once
298
+ * per `retrieve()` with {@link RagContext.auth} (the bound ctx's `auth`); the
299
+ * returned Vectorize metadata filter is merged over the caller's `filter`
300
+ * with **RLS keys winning** (a caller can never widen past what RLS allows),
301
+ * then applied to both the vector and the lexical legs. Return `undefined` to
302
+ * add no constraint (e.g. an admin identity). Runs on retrieval only —
303
+ * indexing is a trusted server path.
304
+ * @example
305
+ * ```ts
306
+ * const docs = defineRag({
307
+ * index: "docs",
308
+ * // only ever return the caller's own org, whatever else they ask for:
309
+ * rlsFilter: (auth) => ({ orgId: (auth as { orgId: string }).orgId }),
310
+ * });
311
+ * ```
312
+ */
313
+ rlsFilter?: (auth: unknown) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
314
+ /** Chunk-text storage override — see {@link RagTextStore}. */
315
+ textStore?: RagTextStore;
316
+ /** Default retrieval depth. Default 5. Capped at 20 (metadata mode) / 100 (text-store mode). */
317
+ topK?: number;
318
+ }
319
+ /**
320
+ * `IndexInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
321
+ * @experimental
322
+ */
323
+ interface IndexInput {
324
+ /**
325
+ * When `false`, throws if the source text produces zero chunks (e.g. empty
326
+ * or whitespace-only text). Default `true` (silently produces zero chunks).
327
+ */
328
+ allowEmptySources?: boolean;
329
+ /** Source document id — chunk ids derive from it as `${id}#${chunkIndex}`. */
330
+ id: string;
331
+ /**
332
+ * Relative weight in `[0, 1]` multiplied into this source's match scores at
333
+ * retrieval time (default 1). Lets canonical docs outrank incidental ones.
334
+ */
335
+ importance?: number;
336
+ /** Source metadata copied onto every chunk vector (e.g. title, url). */
337
+ metadata?: Record<string, unknown>;
338
+ /** Tenant/shard key. Required for multi-tenant apps — Vectorize is account-global. */
339
+ namespace?: string;
340
+ /**
341
+ * Called after each chunk is successfully upserted. Useful for progress
342
+ * tracking during large indexing operations — e.g. updating a UI progress
343
+ * bar or logging per-chunk status.
344
+ */
345
+ onChunk?: (info: {
346
+ chunkIndex: number;
347
+ id: string;
348
+ text: string;
349
+ total: number;
350
+ }) => void;
351
+ /** The document body to chunk + embed + upsert. */
352
+ text: string;
353
+ }
354
+ /**
355
+ * `IndexResult` is part of the experimental `@lunora/ai` API and may change without a major version bump.
356
+ * @experimental
357
+ */
358
+ interface IndexResult {
359
+ /** Number of chunks the source is indexed into. */
360
+ chunks: number;
361
+ /** The deterministic chunk vector ids, in chunk order. */
362
+ ids: ReadonlyArray<string>;
363
+ /**
364
+ * True when the source's content hash matched the previously indexed hash —
365
+ * chunking/embedding/upserts were skipped entirely (a no-op re-sync).
366
+ */
367
+ unchanged: boolean;
368
+ }
369
+ /**
370
+ * `RemoveInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
371
+ * @experimental
372
+ */
373
+ interface RemoveInput {
374
+ /** The source document id whose chunks are removed. */
375
+ id: string;
376
+ namespace?: string;
377
+ }
378
+ /**
379
+ * `RetrieveOptions` is part of the experimental `@lunora/ai` API and may change without a major version bump.
380
+ * @experimental
381
+ */
382
+ interface RetrieveOptions {
383
+ /**
384
+ * Also return this many neighbouring chunks around each match (fetched by
385
+ * deterministic id, not re-queried) — "embed small, retrieve big". Neighbour
386
+ * text is stitched into the chunk's `text` in document order. Best combined
387
+ * with `chunkOverlap: 0`, since overlapping windows repeat boundary text.
388
+ */
389
+ chunkContext?: {
390
+ after?: number;
391
+ before?: number;
392
+ };
393
+ /**
394
+ * Vectorize filter expression — or the name of a pre-defined filter declared
395
+ * in `RagConfig.filters`. Passing a name that is not registered throws at
396
+ * call time, catching spelling mistakes early.
397
+ */
398
+ filter?: Record<string, unknown> | string;
399
+ /** Drop matches whose (importance-adjusted) score falls below this threshold. */
400
+ minScore?: number;
401
+ namespace?: string;
402
+ /**
403
+ * Fires after retrieval completes, before chunk expansion. Useful for
404
+ * observability — logging query latency, hit counts, etc.
405
+ */
406
+ onRetrieve?: (info: {
407
+ matches: number;
408
+ query: string;
409
+ }) => void;
410
+ topK?: number;
411
+ }
412
+ /**
413
+ * `RetrievedChunk` is part of the experimental `@lunora/ai` API and may change without a major version bump.
414
+ * @experimental
415
+ */
416
+ interface RetrievedChunk {
417
+ chunkIndex: number;
418
+ id: string;
419
+ /**
420
+ * The source-level importance weight that was multiplied into this chunk's
421
+ * score. `1` when no importance was set at index time.
422
+ */
423
+ importance: number;
424
+ /** Caller metadata stored on the vector (internal `__rag*` keys stripped). */
425
+ metadata?: Record<string, unknown>;
426
+ /** Cosine similarity, multiplied by the source's `importance` when one was set. */
427
+ score: number;
428
+ sourceId: string;
429
+ text: string;
430
+ }
431
+ /**
432
+ * `RagSource` is part of the experimental `@lunora/ai` API and may change without a major version bump.
433
+ * @experimental
434
+ */
435
+ interface RagSource {
436
+ id: string;
437
+ /** Caller metadata from the source's first-seen chunk (internal keys stripped). */
438
+ metadata?: Record<string, unknown>;
439
+ /**
440
+ * The source's importance weight (the `importance` value passed at index
441
+ * time, default 1), propagated so downstream consumers can factor it into
442
+ * their own ranking or UI.
443
+ */
444
+ weight?: number;
445
+ }
446
+ /**
447
+ * The retrieve return shape — designed so an agent memory step consumes it directly.
448
+ * @experimental
449
+ */
450
+ interface RetrieveResult {
451
+ /** Ranked chunks (best first). */
452
+ chunks: ReadonlyArray<RetrievedChunk>;
453
+ /** Ready-to-inject prompt context: chunks joined under `[source:&lt;id>#&lt;n>]` headers. */
454
+ context: string;
455
+ /** Deduped source references, in first-seen (best) order. */
456
+ sources: ReadonlyArray<RagSource>;
457
+ }
458
+ /**
459
+ * `RagToolOptions` is part of the experimental `@lunora/ai` API and may change without a major version bump.
460
+ * @experimental
461
+ */
462
+ interface RagToolOptions {
463
+ /** Tool description shown to the model. Defaults to a search description naming the index. */
464
+ description?: string;
465
+ /** Namespace applied to every tool-invoked retrieval (the tenant key). */
466
+ namespace?: string;
467
+ /** Retrieval depth for tool-invoked retrievals. */
468
+ topK?: number;
469
+ }
470
+ /**
471
+ * The per-request RAG surface returned by binding a ctx: `docs(ctx)`.
472
+ * @experimental
473
+ */
474
+ interface Rag {
475
+ /**
476
+ * Expose `retrieve` as an AI SDK tool (for `generateText`/`streamText`
477
+ * `tools:` maps), so a model can decide to search the index itself.
478
+ */
479
+ asTool: (options?: RagToolOptions) => Tool<{
480
+ query: string;
481
+ }, RetrieveResult>;
482
+ /**
483
+ * Chunk + embed + upsert one source document. Re-indexing the same `id` is
484
+ * an atomic-enough replace: unchanged content short-circuits via content
485
+ * hash, and stale chunks beyond the new count are deleted automatically.
486
+ */
487
+ index: (input: IndexInput) => Promise<IndexResult>;
488
+ /** Delete every chunk of a previously indexed source. */
489
+ remove: (input: RemoveInput) => Promise<void>;
490
+ /** Embed the query and return ranked chunks + prompt-ready context. */
491
+ retrieve: (query: string, options?: RetrieveOptions) => Promise<RetrieveResult>;
492
+ }
493
+ declare const defineRag: (config: RagConfig) => ((context: RagContext) => Rag);
494
+ /**
495
+ * Guess a MIME type from a file extension. Lowercases and strips a leading `.`
496
+ * from `extension`; returns `"application/octet-stream"` for unknown extensions.
497
+ *
498
+ * Covers the broad set of extensions users are likely to encounter in a web /
499
+ * document-processing context — images, video, audio, office docs, PDF, text,
500
+ * archives, and source code. Follows the same approach as Convex's
501
+ * `guessMimeType` helper.
502
+ * @experimental
503
+ */
504
+ declare const guessMimeTypeFromExtension: (extension: string) => string;
505
+ /**
506
+ * SHA-256 hex digest of binary data. Accepts a `BufferSource` (`ArrayBuffer` or
507
+ * `ArrayBufferView` such as `Uint8Array`). Useful for content-addressable
508
+ * storage — pair with `IndexInput.text` to detect duplicates across re-indexes.
509
+ * @experimental
510
+ */
511
+ declare const contentHash: (data: BufferSource) => Promise<string>;
512
+ /**
513
+ * Reciprocal Rank Fusion (RRF): merge two ranked lists of chunks by their
514
+ * _rank position_ rather than their absolute scores, which are not comparable
515
+ * across different search methods (cosine vs BM25).
516
+ *
517
+ * Each result set contributes `1 / (k + rank)` to each chunk's fused score,
518
+ * where `rank` is 0-based position in the list. The constant `k` (default 60)
519
+ * dampens the influence of high ranks — the standard value from the RRF
520
+ * literature that works well across domains.
521
+ *
522
+ * The fused list is sorted descending by fused score. Ties are broken by
523
+ * preferring the chunk ranked higher in the vector search result (typically
524
+ * the more semantically accurate of the two methods).
525
+ *
526
+ * Callers MUST ensure every chunk in both lists carries a unique, comparable
527
+ * `id` — this is guaranteed by the chunk-id scheme `${sourceId}#${chunkIndex}`.
528
+ * @experimental
529
+ */
530
+ declare const hybridRank: (vectorResults: ReadonlyArray<RetrievedChunk>, textResults: ReadonlyArray<RetrievedChunk>, k?: number) => ReadonlyArray<RetrievedChunk>;
531
+ /**
532
+ * An **in-memory** Okapi BM25 lexical store — the reference adapter behind
533
+ * `RagConfig.lexicalStore`, giving hybrid retrieval its keyword leg with zero
534
+ * infrastructure. State lives in the worker isolate: it is **not durable and not
535
+ * shared across isolates**, so it is intended for tests, local development, and
536
+ * single-isolate workloads. Production deployments plug a durable
537
+ * {@link RagLexicalStore} (a DO-SQLite inverted index, D1, or an external search
538
+ * service) behind the same seam.
539
+ *
540
+ * Tenant isolation is by `namespace` (each namespace keeps its own index). This
541
+ * store holds **no metadata**, so it cannot evaluate a metadata `filter`
542
+ * (including an `rlsFilter` result): when `search` is called with a non-empty
543
+ * filter it **fails closed** — returns no lexical hits and warns once — rather
544
+ * than risk surfacing a row the filter would exclude. If your RLS is
545
+ * metadata-based (not namespace-based) and you want a lexical leg, fold the RLS
546
+ * dimension into the `namespace` or plug a filter-aware store.
547
+ * @experimental
548
+ */
549
+ declare const bm25LexicalStore: () => RagLexicalStore;
550
+ export { type IndexInput, type IndexResult, type LexicalMatch, type Rag, type RagConfig, type RagContext, type RagEmbedder, type RagLexicalStore, type RagNamedFilter, type RagSource, type RagTextStore, type RagToolOptions, type RagVectorMatch, type RagVectorMatches, type RagVectorQueryInput, type RagVectorRecord, type RagVectorUpsertInput, type RagVectors, type RemoveInput, type RetrieveOptions, type RetrieveResult, type RetrievedChunk, type StoredRagChunk, bm25LexicalStore, contentHash, defineRag, fixedWindowChunks, guessMimeTypeFromExtension, hybridRank };