@ganju/utils 0.0.15 → 0.0.16

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.
Files changed (57) hide show
  1. package/dist/constants.d.ts +23 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +95 -8
  4. package/dist/data.d.ts +9 -0
  5. package/dist/data.d.ts.map +1 -0
  6. package/dist/data.js +78 -0
  7. package/dist/dataApi.d.ts +511 -0
  8. package/dist/dataApi.d.ts.map +1 -0
  9. package/dist/dataApi.js +355 -0
  10. package/dist/dataCompiler.d.ts +68 -0
  11. package/dist/dataCompiler.d.ts.map +1 -0
  12. package/dist/dataCompiler.js +726 -0
  13. package/dist/dataDdl.d.ts +29 -0
  14. package/dist/dataDdl.d.ts.map +1 -0
  15. package/dist/dataDdl.js +354 -0
  16. package/dist/dataEngine.d.ts +143 -0
  17. package/dist/dataEngine.d.ts.map +1 -0
  18. package/dist/dataEngine.js +2022 -0
  19. package/dist/dataIdentifiers.d.ts +8 -0
  20. package/dist/dataIdentifiers.d.ts.map +1 -0
  21. package/dist/dataIdentifiers.js +55 -0
  22. package/dist/dataImport.d.ts +77 -0
  23. package/dist/dataImport.d.ts.map +1 -0
  24. package/dist/dataImport.js +450 -0
  25. package/dist/dataRpc.d.ts +131 -0
  26. package/dist/dataRpc.d.ts.map +1 -0
  27. package/dist/dataRpc.js +2 -0
  28. package/dist/dataTypes.d.ts +157 -0
  29. package/dist/dataTypes.d.ts.map +1 -0
  30. package/dist/dataTypes.js +160 -0
  31. package/dist/index.d.ts +308 -6
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/oauth.d.ts +14 -0
  35. package/dist/oauth.d.ts.map +1 -1
  36. package/dist/oauth.js +42 -2
  37. package/dist/resourceRerank.d.ts +3 -2
  38. package/dist/resourceRerank.d.ts.map +1 -1
  39. package/dist/resourceRerank.js +5 -2
  40. package/dist/schema.d.ts +285 -4
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +203 -0
  43. package/package.json +17 -2
  44. package/src/constants.ts +108 -8
  45. package/src/data.ts +74 -0
  46. package/src/dataApi.ts +428 -0
  47. package/src/dataCompiler.ts +948 -0
  48. package/src/dataDdl.ts +419 -0
  49. package/src/dataEngine.ts +2753 -0
  50. package/src/dataIdentifiers.ts +63 -0
  51. package/src/dataImport.ts +554 -0
  52. package/src/dataRpc.ts +121 -0
  53. package/src/dataTypes.ts +312 -0
  54. package/src/index.ts +4 -0
  55. package/src/oauth.ts +42 -1
  56. package/src/resourceRerank.ts +5 -2
  57. package/src/schema.ts +246 -0
package/src/dataRpc.ts ADDED
@@ -0,0 +1,121 @@
1
+ // The methods a project's database object answers over RPC. apps/data's
2
+ // ProjectDatabase implements it; apps/api, apps/mcp and apps/tool-broker call
3
+ // it through databaseStub. Every method resolves to a DataResult.
4
+ import type {
5
+ DataCaller,
6
+ DataDeleteImpact,
7
+ DataResult,
8
+ DataSchema,
9
+ DataTable,
10
+ DataTableCopy
11
+ } from './dataTypes';
12
+
13
+ export type ExportFormat = 'jsonl' | 'csv';
14
+
15
+ export interface ExportFile {
16
+ // null for schema.json
17
+ table: string | null;
18
+ key: string;
19
+ rows: number;
20
+ bytes: number;
21
+ }
22
+
23
+ export interface ExportState {
24
+ id: string;
25
+ format: ExportFormat;
26
+ status: 'running' | 'done' | 'failed';
27
+ startedAt: string;
28
+ finishedAt: string | null;
29
+ actor: string;
30
+ tables: string[];
31
+ tableIndex: number;
32
+ afterId: number;
33
+ upload: {
34
+ key: string;
35
+ uploadId: string;
36
+ parts: { partNumber: number; etag: string }[];
37
+ rows: number;
38
+ bytes: number;
39
+ // Bytes read but not yet uploaded, kept in R2 between steps: every part
40
+ // but the last must be exactly the same size.
41
+ carryKey: string | null;
42
+ } | null;
43
+ files: ExportFile[];
44
+ error: string | null;
45
+ }
46
+
47
+ export interface DatabaseInfo {
48
+ artifactId: string | null;
49
+ name: string | null;
50
+ initializedAt: string | null;
51
+ deletedAt: string | null;
52
+ firstWriteAt: string | null;
53
+ schemaVersion: number;
54
+ size: number;
55
+ tables: { name: string; label: string | null; columns: number }[];
56
+ // A column change copying a large table in the background: table changes,
57
+ // and writes that would change its rows, wait while it runs.
58
+ copy: DataTableCopy | null;
59
+ }
60
+
61
+ export interface RestoreResult {
62
+ bookmark: string;
63
+ undoBookmark: string;
64
+ }
65
+
66
+ type R<T> = Promise<DataResult<T>>;
67
+
68
+ export interface ProjectDatabaseRpc {
69
+ init(args: { artifactId: string; name: string }): R<DatabaseInfo>;
70
+ info(): R<DatabaseInfo>;
71
+ rename(args: { name: string }): R<DatabaseInfo>;
72
+ size(): R<{ bytes: number }>;
73
+ markDeleted(
74
+ caller: DataCaller
75
+ ): R<{ deletedAt: string | null; bookmark: string }>;
76
+ undelete(caller: DataCaller): R<DatabaseInfo>;
77
+ purge(): R<{ purged: true; bookmark: string }>;
78
+ currentBookmark(): R<{ bookmark: string }>;
79
+ restoreTo(caller: DataCaller, args: { timestamp: number }): R<RestoreResult>;
80
+ restoreToBookmark(args: { bookmark: string }): R<RestoreResult>;
81
+ schemaRead(): R<DataSchema>;
82
+ createTable(caller: DataCaller, input: unknown): R<DataTable>;
83
+ updateTable(caller: DataCaller, table: string, patch: unknown): R<DataTable>;
84
+ dropTable(
85
+ caller: DataCaller,
86
+ table: string
87
+ ): R<{ dropped: string; bookmark: string }>;
88
+ addColumn(caller: DataCaller, table: string, input: unknown): R<DataTable>;
89
+ updateColumn(
90
+ caller: DataCaller,
91
+ table: string,
92
+ column: string,
93
+ patch: unknown
94
+ ): R<{ table: DataTable; bookmark: string | null; copy: DataTableCopy | null }>;
95
+ dropColumn(
96
+ caller: DataCaller,
97
+ table: string,
98
+ column: string
99
+ ): R<{ table: DataTable; bookmark: string; copy: DataTableCopy | null }>;
100
+ // Forgets a failed background copy once the owner has seen why.
101
+ dismissCopy(caller: DataCaller): R<{ copy: null }>;
102
+ describe(caller: DataCaller, args?: unknown): R<unknown>;
103
+ query(caller: DataCaller, args: unknown): R<unknown>;
104
+ write(
105
+ caller: DataCaller,
106
+ args: unknown
107
+ ): R<{ ok: true; results: unknown[] }>;
108
+ deleteImpact(
109
+ caller: DataCaller,
110
+ args: { table: string; ids?: number[] }
111
+ ): R<DataDeleteImpact>;
112
+ history(
113
+ caller: DataCaller,
114
+ args: { table: string; id: number; limit?: number; cursor?: number }
115
+ ): R<unknown>;
116
+ exportStart(
117
+ caller: DataCaller,
118
+ args?: { format?: ExportFormat }
119
+ ): R<{ exportId: string; status: ExportState['status'] }>;
120
+ exportStatus(args?: { exportId?: string }): R<ExportState>;
121
+ }
@@ -0,0 +1,312 @@
1
+ // The shapes and limits shared by a project's database object (apps/data),
2
+ // the workers that call it, and the tests. Pure: nothing here touches SQLite.
3
+
4
+ export const DATA_COLUMN_TYPES = [
5
+ 'text',
6
+ 'long_text',
7
+ 'integer',
8
+ 'decimal',
9
+ 'boolean',
10
+ 'date',
11
+ 'datetime',
12
+ 'enum',
13
+ 'json',
14
+ 'email',
15
+ 'url',
16
+ 'phone',
17
+ 'relation',
18
+ 'file'
19
+ ] as const;
20
+ export type DataColumnType = (typeof DATA_COLUMN_TYPES)[number];
21
+
22
+ // What the assistant may do with a table when the caller is restricted (a
23
+ // channel bot). Owners, the dashboard, the CLI and functions see every table.
24
+ export const DATA_ACCESS_LEVELS = ['none', 'read', 'add', 'full'] as const;
25
+ export type DataAccess = (typeof DATA_ACCESS_LEVELS)[number];
26
+ export const DATA_ACCESS_RANK: Record<DataAccess, number> = {
27
+ none: 0,
28
+ read: 1,
29
+ add: 2,
30
+ full: 3
31
+ };
32
+
33
+ export const DATA_ON_DELETE = ['restrict', 'cascade', 'set_null'] as const;
34
+ export type DataOnDelete = (typeof DATA_ON_DELETE)[number];
35
+
36
+ export type DataDefaultValue = string | number | boolean | null;
37
+
38
+ // A column as the owner defines it: the dashboard's form, `ganju.data.json`
39
+ // and the table-create body all use this shape.
40
+ export interface DataColumnInput {
41
+ name: string;
42
+ type: DataColumnType;
43
+ label?: string | null;
44
+ description?: string | null;
45
+ required?: boolean;
46
+ unique?: boolean;
47
+ filterable?: boolean;
48
+ searchable?: boolean;
49
+ personal?: boolean;
50
+ // enum
51
+ values?: string[];
52
+ // relation: the table it points at, and what deleting that row does
53
+ table?: string;
54
+ onDelete?: DataOnDelete;
55
+ // integer / decimal: the lowest value the column may hold
56
+ min?: number | null;
57
+ default?: DataDefaultValue;
58
+ }
59
+
60
+ export interface DataTableInput {
61
+ name: string;
62
+ label?: string | null;
63
+ description?: string | null;
64
+ displayColumn?: string | null;
65
+ access?: DataAccess;
66
+ customerScoped?: boolean;
67
+ history?: boolean;
68
+ columns: DataColumnInput[];
69
+ }
70
+
71
+ // A column as the database object stores it: the input plus its stable id,
72
+ // which names its indexes and constraints so a rename never orphans them.
73
+ export interface DataColumn {
74
+ id: number;
75
+ name: string;
76
+ type: DataColumnType;
77
+ label: string | null;
78
+ description: string | null;
79
+ required: boolean;
80
+ unique: boolean;
81
+ filterable: boolean;
82
+ searchable: boolean;
83
+ personal: boolean;
84
+ values: string[] | null;
85
+ table: string | null;
86
+ onDelete: DataOnDelete | null;
87
+ min: number | null;
88
+ default: DataDefaultValue | undefined;
89
+ position: number;
90
+ }
91
+
92
+ export interface DataTable {
93
+ id: number;
94
+ name: string;
95
+ label: string | null;
96
+ description: string | null;
97
+ displayColumn: string | null;
98
+ access: DataAccess;
99
+ customerScoped: boolean;
100
+ history: boolean;
101
+ position: number;
102
+ columns: DataColumn[];
103
+ }
104
+
105
+ export interface DataSchema {
106
+ tables: DataTable[];
107
+ }
108
+
109
+ // Who is calling and under which rules. `actor` is recorded on every row a
110
+ // call writes; the storage layer refuses a write without one.
111
+ export interface DataCaller {
112
+ // user:<id> · cli:<tokenId> · mcp:<userId> · channel:<participantId> ·
113
+ // function:<toolName> · trigger:<id> · system:<job>
114
+ actor: string;
115
+ // Apply each table's assistant access. Set for channel bots only.
116
+ restricted?: boolean;
117
+ // Customer-scoped tables: fill and filter `participant_id` with this.
118
+ participantId?: string;
119
+ // Datetimes come back in this zone, with the offset.
120
+ timeZone?: string;
121
+ limits?: DataCallerLimits;
122
+ }
123
+
124
+ export interface DataCallerLimits {
125
+ // Writes that add data stop at this size (the plan's ceiling).
126
+ maxBytes?: number | null;
127
+ maxTables?: number | null;
128
+ // The organization left the paid plan: reads and exports only.
129
+ readOnly?: boolean;
130
+ }
131
+
132
+ export interface DataUsage {
133
+ rowsRead: number;
134
+ rowsWritten: number;
135
+ }
136
+
137
+ export const DATA_ERROR_CODES = [
138
+ 'invalid',
139
+ 'not_found',
140
+ 'forbidden',
141
+ 'conflict',
142
+ 'constraint',
143
+ 'guard',
144
+ 'too_large',
145
+ 'full',
146
+ 'read_only',
147
+ 'deleted',
148
+ 'not_initialised',
149
+ 'restore',
150
+ 'internal'
151
+ ] as const;
152
+ export type DataErrorCode = (typeof DATA_ERROR_CODES)[number];
153
+
154
+ // The one error the database object returns. Its message is written for the
155
+ // person or model that made the call, never raw SQLite text.
156
+ export class DataError extends Error {
157
+ readonly code: DataErrorCode;
158
+ readonly details: Record<string, unknown> | undefined;
159
+
160
+ constructor(
161
+ message: string,
162
+ code: DataErrorCode = 'invalid',
163
+ details?: Record<string, unknown>
164
+ ) {
165
+ super(message);
166
+ this.name = 'DataError';
167
+ this.code = code;
168
+ this.details = details;
169
+ }
170
+
171
+ // What the API answers with; its error handler reads both.
172
+ get status(): 400 | 403 | 404 | 409 | 500 {
173
+ return DATA_ERROR_STATUS[this.code];
174
+ }
175
+
176
+ toBody(): Record<string, unknown> {
177
+ return { code: this.code, ...(this.details ? { details: this.details } : {}) };
178
+ }
179
+ }
180
+
181
+ const DATA_ERROR_STATUS: Record<DataErrorCode, 400 | 403 | 404 | 409 | 500> = {
182
+ invalid: 400,
183
+ not_found: 404,
184
+ forbidden: 403,
185
+ conflict: 409,
186
+ constraint: 409,
187
+ guard: 409,
188
+ too_large: 400,
189
+ full: 409,
190
+ read_only: 403,
191
+ deleted: 409,
192
+ not_initialised: 404,
193
+ restore: 400,
194
+ internal: 500
195
+ };
196
+
197
+ export const isDataError = (error: unknown): error is DataError =>
198
+ error instanceof DataError;
199
+
200
+ // What every database object method returns. Errors travel as values, not
201
+ // throws: an RPC exception keeps only its message, and the caller needs the
202
+ // code and the rows read before it.
203
+ export type DataResult<T> =
204
+ | { ok: true; value: T; usage: DataUsage }
205
+ | {
206
+ ok: false;
207
+ error: {
208
+ code: DataErrorCode;
209
+ message: string;
210
+ details?: Record<string, unknown>;
211
+ };
212
+ usage: DataUsage;
213
+ };
214
+
215
+ // Limits. SQLite in a Durable Object allows 100 columns per table, 100 bound
216
+ // parameters per statement and 100 KB per statement; the rest are ours.
217
+ export const DATA_MAX_PARAMS = 100;
218
+ export const DATA_MAX_STATEMENT_BYTES = 90_000;
219
+ // 100 minus id, created_at, updated_at, version, created_by, updated_by and
220
+ // participant_id.
221
+ export const DATA_MAX_COLUMNS = 93;
222
+ export const DATA_MAX_TABLES = 50;
223
+ export const DATA_DEFAULT_PAGE = 20;
224
+ export const DATA_MAX_PAGE = 500;
225
+ export const DATA_MAX_RESULT_BYTES = 1024 * 1024;
226
+ // Past this many rows, an aggregate or a sort on an unindexed column needs a
227
+ // filter on an indexed one: the object serves one request at a time.
228
+ export const DATA_LARGE_TABLE_ROWS = 200_000;
229
+ export const DATA_MAX_WRITE_OPERATIONS = 50;
230
+ export const DATA_MAX_INSERT_ROWS = 500;
231
+ export const DATA_MAX_ROWS_PER_WRITE = 5_000;
232
+ export const DATA_MAX_VALUE_BYTES = 1_000_000;
233
+ export const DATA_MAX_DESCRIPTION = 300;
234
+ export const DATA_MAX_LABEL = 100;
235
+ export const DATA_MAX_ENUM_VALUES = 100;
236
+ export const DATA_MAX_IN_VALUES = 50;
237
+ export const DATA_MAX_SORT_KEYS = 3;
238
+ export const DATA_MAX_AGGREGATES = 5;
239
+ export const DATA_MAX_GROUP_BY = 3;
240
+ export const DATA_MAX_GROUPS = 100;
241
+ // A type change copies the table in one request up to this size. A larger
242
+ // table is copied in the background, this many rows a step (tens of
243
+ // milliseconds, so other requests are served in between), and writes that
244
+ // would change its rows wait until it is done.
245
+ export const DATA_REBUILD_MAX_ROWS = 100_000;
246
+ export const DATA_COPY_STEP_ROWS = 5_000;
247
+ // A delete preview follows cascades through up to this many rows; past it,
248
+ // its counts are lower bounds.
249
+ export const DATA_IMPACT_MAX_ROWS = 200_000;
250
+ export const DATA_IMPACT_MAX_IDS = DATA_MAX_ROWS_PER_WRITE;
251
+ export const DATA_HISTORY_DAYS = 90;
252
+
253
+ // What deleting rows does elsewhere: one entry per relation column that points
254
+ // at a row the delete removes, cascades followed all the way down. `depth` is
255
+ // how many cascades away from the deleted rows the pointed-at table is.
256
+ export interface DataDeleteImpact {
257
+ table: string;
258
+ rows: number;
259
+ effects: {
260
+ table: string;
261
+ column: string;
262
+ onDelete: DataOnDelete;
263
+ rows: number;
264
+ depth: number;
265
+ }[];
266
+ // A restrict would refuse the delete.
267
+ blocked: boolean;
268
+ // Past DATA_IMPACT_MAX_ROWS: every count is at least what it says.
269
+ truncated: boolean;
270
+ }
271
+
272
+ // A table copy running in the background, as callers see it.
273
+ export interface DataTableCopy {
274
+ table: string;
275
+ // The column whose change or drop started it.
276
+ column: string;
277
+ kind: 'change' | 'drop';
278
+ rows: number;
279
+ copied: number;
280
+ startedAt: string;
281
+ actor: string;
282
+ status: 'running' | 'failed';
283
+ error: string | null;
284
+ }
285
+
286
+ // The table as the owner would write it again: what `ganju.data.json` and an
287
+ // export's schema.json hold.
288
+ export const toTableInput = (table: DataTable): DataTableInput => ({
289
+ name: table.name,
290
+ label: table.label,
291
+ description: table.description,
292
+ displayColumn: table.displayColumn,
293
+ access: table.access,
294
+ customerScoped: table.customerScoped,
295
+ history: table.history,
296
+ columns: table.columns.map(column => {
297
+ const out: DataColumnInput = { name: column.name, type: column.type };
298
+ if (column.label) out.label = column.label;
299
+ if (column.description) out.description = column.description;
300
+ if (column.required) out.required = true;
301
+ if (column.unique) out.unique = true;
302
+ if (column.filterable) out.filterable = true;
303
+ if (column.searchable) out.searchable = true;
304
+ if (column.personal) out.personal = true;
305
+ if (column.values) out.values = column.values;
306
+ if (column.table) out.table = column.table;
307
+ if (column.onDelete) out.onDelete = column.onDelete;
308
+ if (column.min !== null) out.min = column.min;
309
+ if (column.default !== undefined) out.default = column.default;
310
+ return out;
311
+ })
312
+ });
package/src/index.ts CHANGED
@@ -344,6 +344,8 @@ import { citesUrl, citedPages, selectCitedSources } from './citations';
344
344
  import {
345
345
  refreshOAuthToken,
346
346
  OAuthReauthRequiredError,
347
+ OAuthRefreshError,
348
+ refreshFailureReauthReason,
347
349
  buildReauthMetadata,
348
350
  clearReauthMetadata,
349
351
  isCredentialNeedingReauth
@@ -557,6 +559,8 @@ export const utils = {
557
559
  selectCitedSources,
558
560
  refreshOAuthToken,
559
561
  OAuthReauthRequiredError,
562
+ OAuthRefreshError,
563
+ refreshFailureReauthReason,
560
564
  buildReauthMetadata,
561
565
  clearReauthMetadata,
562
566
  isCredentialNeedingReauth,
package/src/oauth.ts CHANGED
@@ -24,6 +24,21 @@ export class OAuthReauthRequiredError extends Error {
24
24
  }
25
25
  }
26
26
 
27
+ // The token endpoint answered with an error that isn't a reauth code. `status`
28
+ // tells a rejection (4xx) from an outage (5xx); a network failure never gets
29
+ // here, it throws fetch's own error.
30
+ export class OAuthRefreshError extends Error {
31
+ readonly status: number;
32
+ readonly code: string | undefined;
33
+
34
+ constructor(status: number, code: string | undefined, body: string) {
35
+ super(`oauth refresh failed (${status}): ${body}`);
36
+ this.name = 'OAuthRefreshError';
37
+ this.status = status;
38
+ this.code = code;
39
+ }
40
+ }
41
+
27
42
  const parseErrorCode = (body: string): string | undefined => {
28
43
  try {
29
44
  return (JSON.parse(body) as { error?: string })?.error;
@@ -52,7 +67,7 @@ export const refreshOAuthToken = async (
52
67
  if (code && constants.REAUTH_ERROR_CODES.includes(code)) {
53
68
  throw new OAuthReauthRequiredError(code);
54
69
  }
55
- throw new Error(`oauth refresh failed (${response.status}): ${body}`);
70
+ throw new OAuthRefreshError(response.status, code, body);
56
71
  }
57
72
 
58
73
  const tokens = (await response.json()) as {
@@ -95,6 +110,32 @@ export const clearReauthMetadata = (
95
110
  return Object.keys(next).length > 0 ? next : null;
96
111
  };
97
112
 
113
+ /**
114
+ * Why a failed refresh should flag the credential for a reconnect, or null
115
+ * when it shouldn't. A reauth code flags it at once. Any other rejection
116
+ * (4xx) flags it only once the access token has been expired for
117
+ * CREDENTIAL_REFRESH_GIVE_UP_MS, so a one-off doesn't. Never flagged: an
118
+ * outage (5xx), a network error, rate limiting (429), and errors about our
119
+ * own client id / secret, which no reconnect can fix.
120
+ */
121
+ export const refreshFailureReauthReason = (
122
+ err: unknown,
123
+ expiresAt: Date | null,
124
+ now = Date.now()
125
+ ): string | null => {
126
+ if (err instanceof OAuthReauthRequiredError) return err.code;
127
+ if (!(err instanceof OAuthRefreshError)) return null;
128
+ if (err.status < 400 || err.status >= 500 || err.status === 429) return null;
129
+ if (err.code && constants.OAUTH_CLIENT_ERROR_CODES.includes(err.code)) {
130
+ return null;
131
+ }
132
+ if (!expiresAt) return null;
133
+ if (now - expiresAt.getTime() < constants.CREDENTIAL_REFRESH_GIVE_UP_MS) {
134
+ return null;
135
+ }
136
+ return err.code ? `refresh_rejected:${err.code}` : 'refresh_rejected';
137
+ };
138
+
98
139
  export const isCredentialNeedingReauth = (metadata: unknown): boolean => {
99
140
  if (!metadata || typeof metadata !== 'object') return false;
100
141
  return (metadata as Record<string, unknown>).needsReauth === true;
@@ -15,7 +15,8 @@ type RerankResponse = {
15
15
  };
16
16
 
17
17
  /**
18
- * The cross-encoder second stage for resource search, or undefined when the
18
+ * The cross-encoder second stage for resource search, or undefined when it is
19
+ * off (RESOURCE_SEARCH_RERANK_ENABLED, unless `enabled` overrides it) or the
19
20
  * Worker has no `AI` binding — local dev without Workers AI keeps plain fusion.
20
21
  *
21
22
  * Returns one score per document, in input order. A response that doesn't
@@ -23,10 +24,12 @@ type RerankResponse = {
23
24
  * ranking would silently drop the chunks it left out.
24
25
  */
25
26
  export const createResourceReranker = (
26
- source: EnvSource
27
+ source: EnvSource,
28
+ enabled: boolean = constants.RESOURCE_SEARCH_RERANK_ENABLED
27
29
  ):
28
30
  | ((query: string, documents: string[]) => Promise<number[] | null>)
29
31
  | undefined => {
32
+ if (!enabled) return undefined;
30
33
  const ai = (source.env as { AI?: RerankAiBinding } | undefined)?.AI;
31
34
  if (!ai) return undefined;
32
35