@librechat/agents 3.7.8 → 3.7.10

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 (161) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +2 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +40 -6
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/executeHooks.cjs +8 -0
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  11. package/dist/cjs/hooks/index.cjs +2 -0
  12. package/dist/cjs/hooks/index.cjs.map +1 -1
  13. package/dist/cjs/hooks/types.cjs +1 -0
  14. package/dist/cjs/hooks/types.cjs.map +1 -1
  15. package/dist/cjs/langfuse.cjs +43 -3
  16. package/dist/cjs/langfuse.cjs.map +1 -1
  17. package/dist/cjs/llm/invoke.cjs +2 -6
  18. package/dist/cjs/llm/invoke.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +6 -0
  20. package/dist/cjs/llm/request.cjs.map +1 -1
  21. package/dist/cjs/main.cjs +19 -11
  22. package/dist/cjs/messages/format.cjs +95 -18
  23. package/dist/cjs/messages/format.cjs.map +1 -1
  24. package/dist/cjs/messages/index.cjs +1 -1
  25. package/dist/cjs/run.cjs +201 -46
  26. package/dist/cjs/run.cjs.map +1 -1
  27. package/dist/cjs/session/AgentSession.cjs +1 -1
  28. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  29. package/dist/cjs/summarization/index.cjs +1 -0
  30. package/dist/cjs/summarization/index.cjs.map +1 -1
  31. package/dist/cjs/summarization/node.cjs +24 -85
  32. package/dist/cjs/summarization/node.cjs.map +1 -1
  33. package/dist/cjs/summarization/semanticIndex.cjs +7 -3
  34. package/dist/cjs/summarization/semanticIndex.cjs.map +1 -1
  35. package/dist/cjs/summarization/shared.cjs +83 -0
  36. package/dist/cjs/summarization/shared.cjs.map +1 -0
  37. package/dist/cjs/tools/ToolNode.cjs +5 -5
  38. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  39. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  40. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1 -1
  41. package/dist/cjs/tools/runStepResume.cjs +1 -1
  42. package/dist/cjs/tools/runStepResume.cjs.map +1 -1
  43. package/dist/cjs/tools/search/metrics.cjs +234 -0
  44. package/dist/cjs/tools/search/metrics.cjs.map +1 -0
  45. package/dist/cjs/tools/search/rerankers.cjs +80 -68
  46. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  47. package/dist/cjs/tools/search/search.cjs +95 -45
  48. package/dist/cjs/tools/search/search.cjs.map +1 -1
  49. package/dist/cjs/tools/search/tool.cjs +77 -49
  50. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  51. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  52. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +8 -12
  53. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  54. package/dist/cjs/utils/tokens.cjs +8 -1
  55. package/dist/cjs/utils/tokens.cjs.map +1 -1
  56. package/dist/esm/agents/AgentContext.mjs +3 -2
  57. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  58. package/dist/esm/common/constants.mjs +2 -1
  59. package/dist/esm/common/constants.mjs.map +1 -1
  60. package/dist/esm/graphs/Graph.mjs +40 -6
  61. package/dist/esm/graphs/Graph.mjs.map +1 -1
  62. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  63. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  64. package/dist/esm/hooks/executeHooks.mjs +8 -1
  65. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  66. package/dist/esm/hooks/index.mjs +2 -1
  67. package/dist/esm/hooks/index.mjs.map +1 -1
  68. package/dist/esm/hooks/types.mjs +1 -0
  69. package/dist/esm/hooks/types.mjs.map +1 -1
  70. package/dist/esm/langfuse.mjs +43 -3
  71. package/dist/esm/langfuse.mjs.map +1 -1
  72. package/dist/esm/llm/invoke.mjs +2 -6
  73. package/dist/esm/llm/invoke.mjs.map +1 -1
  74. package/dist/esm/llm/request.mjs +6 -1
  75. package/dist/esm/llm/request.mjs.map +1 -1
  76. package/dist/esm/main.mjs +13 -12
  77. package/dist/esm/messages/format.mjs +96 -19
  78. package/dist/esm/messages/format.mjs.map +1 -1
  79. package/dist/esm/messages/index.mjs +1 -1
  80. package/dist/esm/run.mjs +201 -46
  81. package/dist/esm/run.mjs.map +1 -1
  82. package/dist/esm/session/AgentSession.mjs +1 -1
  83. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  84. package/dist/esm/summarization/index.mjs +1 -0
  85. package/dist/esm/summarization/index.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +21 -82
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/summarization/semanticIndex.mjs +7 -4
  89. package/dist/esm/summarization/semanticIndex.mjs.map +1 -1
  90. package/dist/esm/summarization/shared.mjs +79 -0
  91. package/dist/esm/summarization/shared.mjs.map +1 -0
  92. package/dist/esm/tools/ToolNode.mjs +5 -5
  93. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  94. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  95. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1 -1
  96. package/dist/esm/tools/runStepResume.mjs +1 -1
  97. package/dist/esm/tools/runStepResume.mjs.map +1 -1
  98. package/dist/esm/tools/search/metrics.mjs +234 -0
  99. package/dist/esm/tools/search/metrics.mjs.map +1 -0
  100. package/dist/esm/tools/search/rerankers.mjs +80 -68
  101. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  102. package/dist/esm/tools/search/search.mjs +96 -46
  103. package/dist/esm/tools/search/search.mjs.map +1 -1
  104. package/dist/esm/tools/search/tool.mjs +77 -49
  105. package/dist/esm/tools/search/tool.mjs.map +1 -1
  106. package/dist/esm/tools/search/utils.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutor.mjs +8 -12
  108. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  109. package/dist/esm/utils/tokens.mjs +8 -2
  110. package/dist/esm/utils/tokens.mjs.map +1 -1
  111. package/dist/types/common/constants.d.ts +7 -0
  112. package/dist/types/graphs/Graph.d.ts +9 -0
  113. package/dist/types/hooks/executeHooks.d.ts +5 -2
  114. package/dist/types/hooks/index.d.ts +7 -2
  115. package/dist/types/hooks/types.d.ts +45 -7
  116. package/dist/types/langfuse.d.ts +3 -1
  117. package/dist/types/llm/request.d.ts +10 -0
  118. package/dist/types/messages/format.d.ts +8 -2
  119. package/dist/types/run.d.ts +2 -1
  120. package/dist/types/summarization/index.d.ts +6 -0
  121. package/dist/types/summarization/node.d.ts +0 -4
  122. package/dist/types/summarization/semanticIndex.d.ts +2 -0
  123. package/dist/types/summarization/shared.d.ts +25 -0
  124. package/dist/types/tools/search/metrics.d.ts +15 -0
  125. package/dist/types/tools/search/rerankers.d.ts +30 -5
  126. package/dist/types/tools/search/tool.d.ts +7 -1
  127. package/dist/types/tools/search/types.d.ts +88 -4
  128. package/dist/types/tools/search/utils.d.ts +2 -10
  129. package/dist/types/types/run.d.ts +8 -0
  130. package/dist/types/types/stream.d.ts +6 -0
  131. package/dist/types/types/summarize.d.ts +6 -0
  132. package/dist/types/utils/tokens.d.ts +9 -0
  133. package/package.json +1 -1
  134. package/src/agents/AgentContext.ts +2 -5
  135. package/src/common/constants.ts +8 -0
  136. package/src/graphs/Graph.ts +45 -0
  137. package/src/hooks/HookRegistry.ts +3 -1
  138. package/src/hooks/executeHooks.ts +20 -2
  139. package/src/hooks/index.ts +12 -1
  140. package/src/hooks/types.ts +47 -4
  141. package/src/langfuse.ts +70 -0
  142. package/src/llm/invoke.ts +13 -29
  143. package/src/llm/request.ts +24 -0
  144. package/src/messages/format.ts +213 -27
  145. package/src/run.ts +366 -186
  146. package/src/summarization/index.ts +11 -0
  147. package/src/summarization/node.ts +117 -130
  148. package/src/summarization/semanticIndex.ts +16 -7
  149. package/src/summarization/shared.ts +130 -0
  150. package/src/tools/runStepResume.ts +9 -0
  151. package/src/tools/search/metrics.ts +400 -0
  152. package/src/tools/search/rerankers.ts +160 -97
  153. package/src/tools/search/search.ts +139 -56
  154. package/src/tools/search/tool.ts +126 -62
  155. package/src/tools/search/types.ts +104 -4
  156. package/src/tools/search/utils.ts +2 -10
  157. package/src/tools/subagent/SubagentExecutor.ts +4 -27
  158. package/src/types/run.ts +8 -0
  159. package/src/types/stream.ts +6 -0
  160. package/src/types/summarize.ts +7 -0
  161. package/src/utils/tokens.ts +31 -8
@@ -0,0 +1,400 @@
1
+ import type * as t from './types';
2
+
3
+ /** Distinct failure reasons kept per phase before the tail folds into
4
+ * `other`: enough to characterize a mixed failure without letting a
5
+ * pathological run grow one map entry per source. */
6
+ const MAX_REASON_KEYS = 6;
7
+ /** Failing hosts named in a summary before the rest are counted only. */
8
+ const MAX_FAILURE_SAMPLES = 3;
9
+ /** Cap on the raw provider message carried alongside a summary. */
10
+ const MAX_DETAIL_LENGTH = 200;
11
+ /** Cap on a provider-supplied label placed inside a summary line, so a
12
+ * malformed response cannot stretch a fixed-width line without bound. */
13
+ const MAX_LABEL_LENGTH = 48;
14
+ const OTHER_REASON = 'other';
15
+
16
+ const HTTP_STATUS_REGEX = /\b(?:status(?:\scode)?|http)\D{0,3}(\d{3})\b/i;
17
+
18
+ /** Free-form provider messages have unbounded cardinality; the aggregate only
19
+ * keeps a normalized class so the reason map stays small and comparable. */
20
+ const REASON_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [
21
+ [/ECONNABORTED|ETIMEDOUT|timeout|timed out/i, 'timeout'],
22
+ [/ENOTFOUND|EAI_AGAIN|getaddrinfo/i, 'dns'],
23
+ [/ECONNREFUSED|ECONNRESET|EPIPE|socket hang up/i, 'connection'],
24
+ [/CERT_|SSL|TLS|self[- ]signed/i, 'tls'],
25
+ [/abort|cancel/i, 'aborted'],
26
+ ];
27
+
28
+ export const classifyFailure = (message?: string): string => {
29
+ if (message == null || message === '') {
30
+ return 'unknown';
31
+ }
32
+ const status = HTTP_STATUS_REGEX.exec(message);
33
+ if (status != null) {
34
+ return `http_${status[1]}`;
35
+ }
36
+ for (const [pattern, reason] of REASON_PATTERNS) {
37
+ if (pattern.test(message)) {
38
+ return reason;
39
+ }
40
+ }
41
+ return OTHER_REASON;
42
+ };
43
+
44
+ const bump = (counts: Map<string, number>, rawKey: string): void => {
45
+ const key = label(rawKey);
46
+ const existing = counts.get(key);
47
+ if (existing != null) {
48
+ counts.set(key, existing + 1);
49
+ return;
50
+ }
51
+ if (counts.size >= MAX_REASON_KEYS) {
52
+ counts.set(OTHER_REASON, (counts.get(OTHER_REASON) ?? 0) + 1);
53
+ return;
54
+ }
55
+ counts.set(key, 1);
56
+ };
57
+
58
+ const hostOf = (url: string): string => {
59
+ try {
60
+ return new URL(url).hostname.replace(/^www\./, '');
61
+ } catch {
62
+ return url.slice(0, 60);
63
+ }
64
+ };
65
+
66
+ const formatCount = (value: number): string =>
67
+ value < 10000 ? String(value) : `${(value / 1000).toFixed(1)}k`;
68
+
69
+ const formatMs = (ms: number): string =>
70
+ ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
71
+
72
+ const formatMap = (counts: Map<string, number>): string => {
73
+ let out = '';
74
+ for (const [key, count] of counts) {
75
+ out += `${out === '' ? '' : ','}${key}:${count}`;
76
+ }
77
+ return `{${out}}`;
78
+ };
79
+
80
+ const truncate = (value: string, max: number): string =>
81
+ value.length <= max ? value : `${value.slice(0, max)}…`;
82
+
83
+ /** Newlines, bidi overrides, and other non-printing characters let a remote
84
+ * payload split one summary into several physical lines or forge a second
85
+ * apparent entry, so they never survive into a log message. */
86
+ const UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}"]+/gu;
87
+
88
+ const sanitize = (value: string): string => value.replace(UNPRINTABLE, ' ');
89
+
90
+ /** Every provider-supplied string that reaches a summary line or a map key
91
+ * goes through here: the phases promise one bounded line, and neither a
92
+ * remote payload nor an external caller's label is safe on its own. */
93
+ const label = (value: string): string =>
94
+ truncate(sanitize(value), MAX_LABEL_LENGTH);
95
+
96
+ const detailOf = (value: string): string =>
97
+ truncate(sanitize(value), MAX_DETAIL_LENGTH);
98
+
99
+ /** A phase carries one provider label. Nothing in this package mixes them,
100
+ * but a wrong label is worse than an honest one if that ever changes. */
101
+ const mergeProvider = (current: string, incoming: string): string =>
102
+ current === incoming ? current : 'mixed';
103
+
104
+ interface SearchPhase {
105
+ provider: string;
106
+ queries: number;
107
+ failed: number;
108
+ errors: number;
109
+ durationMs: number;
110
+ types: Map<string, number>;
111
+ reasons: Map<string, number>;
112
+ detail?: string;
113
+ }
114
+
115
+ interface ScrapePhase {
116
+ links: number;
117
+ ok: number;
118
+ empty: number;
119
+ chars: number;
120
+ highlights: number;
121
+ failed: number;
122
+ reasons: Map<string, number>;
123
+ samples: string[];
124
+ detail?: string;
125
+ }
126
+
127
+ interface RerankPhase {
128
+ provider: string;
129
+ model?: string;
130
+ calls: number;
131
+ fallbacks: number;
132
+ errors: number;
133
+ chunks: number;
134
+ maxChunks: number;
135
+ dropped: number;
136
+ topK?: number;
137
+ topKLimit?: number;
138
+ results: number;
139
+ units: number;
140
+ durationMs: number;
141
+ maxDurationMs: number;
142
+ reasons: Map<string, number>;
143
+ error?: t.SafeErrorLog;
144
+ }
145
+
146
+ /**
147
+ * Creates the counter set for one `web_search` call.
148
+ *
149
+ * The pipeline reranks once per scraped source and scrapes once per link, so
150
+ * logging at those points scales the log with the result count. Every phase
151
+ * records into fixed-width counters instead and {@link t.SearchMetrics.flush}
152
+ * emits one line per phase, at the highest severity that phase reached.
153
+ *
154
+ * @param autoFlush emit on every record, for a caller with no enclosing search
155
+ * to fold into (a reranker used directly). Recording is synchronous, so the
156
+ * record/flush/reset cycle can never interleave with a concurrent call.
157
+ */
158
+ export const createSearchMetrics = (
159
+ logger: t.Logger,
160
+ autoFlush = false
161
+ ): t.SearchMetrics => {
162
+ let search: SearchPhase | undefined;
163
+ let scrape: ScrapePhase | undefined;
164
+ let rerank: RerankPhase | undefined;
165
+
166
+ const flushSearch = (phase: SearchPhase): void => {
167
+ let line =
168
+ `[web_search] search=${label(phase.provider)}` +
169
+ ` queries=${phase.queries}`;
170
+ if (phase.types.size > 0) {
171
+ line += ` results=${formatMap(phase.types)}`;
172
+ }
173
+ line += ` dur=${formatMs(phase.durationMs)}`;
174
+ if (phase.failed === 0) {
175
+ logger.debug(line);
176
+ return;
177
+ }
178
+ line += ` failed=${phase.failed} reasons=${formatMap(phase.reasons)}`;
179
+ /** A rejected query was an error-level event before it was aggregated;
180
+ * a provider that merely reported failure was not. */
181
+ if (phase.errors === 0) {
182
+ logger.warn(line);
183
+ return;
184
+ }
185
+ if (phase.detail == null) {
186
+ logger.error(line);
187
+ return;
188
+ }
189
+ logger.error(line, phase.detail);
190
+ };
191
+
192
+ const flushScrape = (phase: ScrapePhase): void => {
193
+ let line =
194
+ `[web_search] scrape links=${phase.links} ok=${phase.ok}` +
195
+ ` chars=${formatCount(phase.chars)} highlights=${phase.highlights}`;
196
+ if (phase.empty > 0) {
197
+ line += ` empty=${phase.empty}`;
198
+ }
199
+ if (phase.failed === 0) {
200
+ logger.debug(line);
201
+ return;
202
+ }
203
+ line += ` failed=${phase.failed} reasons=${formatMap(phase.reasons)}`;
204
+ if (phase.samples.length > 0) {
205
+ line += ` sample=${phase.samples.join(',')}`;
206
+ }
207
+ if (phase.detail == null) {
208
+ logger.error(line);
209
+ return;
210
+ }
211
+ logger.error(line, phase.detail);
212
+ };
213
+
214
+ const flushRerank = (phase: RerankPhase): void => {
215
+ let line =
216
+ `[web_search] rerank=${label(phase.provider)} calls=${phase.calls}` +
217
+ ` chunks=${phase.chunks} maxChunks=${phase.maxChunks}` +
218
+ ` results=${phase.results}` +
219
+ ` dur=${formatMs(phase.durationMs)}` +
220
+ ` maxDur=${formatMs(phase.maxDurationMs)}`;
221
+ if (phase.model != null) {
222
+ /** The only free-form remote value in the line, so it is the one field
223
+ * that gets delimited — a truncated payload cannot then read as
224
+ * further fields. */
225
+ line += ` model="${label(phase.model)}"`;
226
+ }
227
+ if (phase.units > 0) {
228
+ line += ` units=${phase.units}`;
229
+ }
230
+ if (phase.dropped > 0) {
231
+ line += ` dropped=${phase.dropped}`;
232
+ }
233
+ if (phase.topKLimit != null) {
234
+ line += ` topK=${phase.topK} topKLimit=${phase.topKLimit}`;
235
+ }
236
+ if (phase.fallbacks === 0) {
237
+ logger.debug(line);
238
+ return;
239
+ }
240
+ line += ` fallbacks=${phase.fallbacks} reasons=${formatMap(phase.reasons)}`;
241
+ /** A placeholder reranker returns input order by design, so a phase whose
242
+ * only fallbacks are placeholders is the configuration working — it stays
243
+ * at the debug level it had before these counters existed. */
244
+ const degraded = phase.fallbacks - (phase.reasons.get('placeholder') ?? 0);
245
+ if (degraded === 0 && phase.errors === 0) {
246
+ logger.debug(line);
247
+ return;
248
+ }
249
+ if (phase.errors === 0) {
250
+ logger.warn(line);
251
+ return;
252
+ }
253
+ if (phase.error == null) {
254
+ logger.error(line);
255
+ return;
256
+ }
257
+ logger.error(line, phase.error);
258
+ };
259
+
260
+ const flush = (): void => {
261
+ if (search != null) {
262
+ flushSearch(search);
263
+ search = undefined;
264
+ }
265
+ if (scrape != null) {
266
+ flushScrape(scrape);
267
+ scrape = undefined;
268
+ }
269
+ if (rerank != null) {
270
+ flushRerank(rerank);
271
+ rerank = undefined;
272
+ }
273
+ };
274
+
275
+ const recordSearch = (observation: t.SearchObservation): void => {
276
+ search ??= {
277
+ provider: observation.provider,
278
+ queries: 0,
279
+ failed: 0,
280
+ errors: 0,
281
+ durationMs: 0,
282
+ types: new Map(),
283
+ reasons: new Map(),
284
+ };
285
+ search.provider = mergeProvider(search.provider, observation.provider);
286
+ search.queries += 1;
287
+ /** Sub-searches run concurrently, so the phase lasts as long as its
288
+ * slowest query rather than the sum of all of them. */
289
+ search.durationMs = Math.max(search.durationMs, observation.durationMs);
290
+ if (observation.error != null) {
291
+ search.failed += 1;
292
+ /** Provider messages are prose: classify before counting, or equivalent
293
+ * failures never collapse and the raw text lands in the summary. */
294
+ bump(
295
+ search.reasons,
296
+ `${observation.type}:${classifyFailure(observation.error)}`
297
+ );
298
+ /** `detail` is only ever logged on the error path, so it is captured
299
+ * only from a thrown observation — otherwise an earlier soft failure's
300
+ * message would be attached to an exception it has nothing to do
301
+ * with, and the real one dropped. */
302
+ if (observation.thrown === true) {
303
+ search.errors += 1;
304
+ search.detail ??= detailOf(observation.error);
305
+ }
306
+ } else if (observation.results > 0) {
307
+ const type = label(observation.type);
308
+ const seen = search.types.get(type) ?? 0;
309
+ search.types.set(type, seen + observation.results);
310
+ }
311
+ if (autoFlush) {
312
+ flush();
313
+ }
314
+ };
315
+
316
+ const recordScrape = (observation: t.ScrapeObservation): void => {
317
+ scrape ??= {
318
+ links: 0,
319
+ ok: 0,
320
+ empty: 0,
321
+ chars: 0,
322
+ highlights: 0,
323
+ failed: 0,
324
+ reasons: new Map(),
325
+ samples: [],
326
+ detail: undefined,
327
+ };
328
+ scrape.links += 1;
329
+ if (observation.error != null) {
330
+ scrape.failed += 1;
331
+ const reason = classifyFailure(observation.error);
332
+ bump(scrape.reasons, reason);
333
+ if (scrape.samples.length < MAX_FAILURE_SAMPLES) {
334
+ scrape.samples.push(label(`${hostOf(observation.url)}:${reason}`));
335
+ }
336
+ scrape.detail ??= detailOf(observation.error);
337
+ } else {
338
+ scrape.ok += 1;
339
+ const chars = observation.chars ?? 0;
340
+ scrape.chars += chars;
341
+ scrape.highlights += observation.highlights ?? 0;
342
+ if (chars === 0) {
343
+ scrape.empty += 1;
344
+ }
345
+ }
346
+ if (autoFlush) {
347
+ flush();
348
+ }
349
+ };
350
+
351
+ const recordRerank = (observation: t.RerankObservation): void => {
352
+ rerank ??= {
353
+ provider: observation.provider,
354
+ calls: 0,
355
+ fallbacks: 0,
356
+ errors: 0,
357
+ chunks: 0,
358
+ maxChunks: 0,
359
+ dropped: 0,
360
+ results: 0,
361
+ units: 0,
362
+ durationMs: 0,
363
+ maxDurationMs: 0,
364
+ reasons: new Map(),
365
+ };
366
+ rerank.provider = mergeProvider(rerank.provider, observation.provider);
367
+ rerank.calls += 1;
368
+ rerank.chunks += observation.chunks;
369
+ rerank.maxChunks = Math.max(rerank.maxChunks, observation.chunks);
370
+ rerank.results += observation.results;
371
+ rerank.dropped += observation.dropped ?? 0;
372
+ rerank.topK ??= observation.topK;
373
+ rerank.topKLimit ??= observation.topKLimit;
374
+ rerank.units += observation.units ?? 0;
375
+ rerank.durationMs += observation.durationMs;
376
+ rerank.maxDurationMs = Math.max(
377
+ rerank.maxDurationMs,
378
+ observation.durationMs
379
+ );
380
+ if (observation.model != null) {
381
+ rerank.model =
382
+ rerank.model == null
383
+ ? observation.model
384
+ : mergeProvider(rerank.model, observation.model);
385
+ }
386
+ if (observation.reason != null) {
387
+ rerank.fallbacks += 1;
388
+ bump(rerank.reasons, observation.reason);
389
+ }
390
+ if (observation.error != null) {
391
+ rerank.errors += 1;
392
+ rerank.error ??= observation.error;
393
+ }
394
+ if (autoFlush) {
395
+ flush();
396
+ }
397
+ };
398
+
399
+ return { recordSearch, recordScrape, recordRerank, flush };
400
+ };