@librechat/agents 3.3.3 → 3.3.5

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 (274) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +21 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +10 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +793 -111
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/index.cjs +12 -0
  14. package/dist/cjs/hooks/index.cjs.map +1 -1
  15. package/dist/cjs/hooks/types.cjs +1 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -1
  17. package/dist/cjs/langfuse.cjs +8 -0
  18. package/dist/cjs/langfuse.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  20. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  21. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
  22. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  24. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  25. package/dist/cjs/llm/invoke.cjs +331 -5
  26. package/dist/cjs/llm/invoke.cjs.map +1 -1
  27. package/dist/cjs/llm/openai/index.cjs +41 -5
  28. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  29. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  30. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  31. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  32. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  33. package/dist/cjs/llm/preempt.cjs +132 -0
  34. package/dist/cjs/llm/preempt.cjs.map +1 -0
  35. package/dist/cjs/main.cjs +36 -5
  36. package/dist/cjs/messages/alternation.cjs +92 -0
  37. package/dist/cjs/messages/alternation.cjs.map +1 -0
  38. package/dist/cjs/messages/cache.cjs +54 -33
  39. package/dist/cjs/messages/cache.cjs.map +1 -1
  40. package/dist/cjs/messages/contextPruning.cjs +17 -43
  41. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  42. package/dist/cjs/messages/core.cjs +315 -23
  43. package/dist/cjs/messages/core.cjs.map +1 -1
  44. package/dist/cjs/messages/format.cjs +290 -60
  45. package/dist/cjs/messages/format.cjs.map +1 -1
  46. package/dist/cjs/messages/handoffCue.cjs +63 -0
  47. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  48. package/dist/cjs/messages/index.cjs +4 -1
  49. package/dist/cjs/messages/injected.cjs +60 -0
  50. package/dist/cjs/messages/injected.cjs.map +1 -0
  51. package/dist/cjs/messages/prune.cjs +503 -127
  52. package/dist/cjs/messages/prune.cjs.map +1 -1
  53. package/dist/cjs/run.cjs +80 -7
  54. package/dist/cjs/run.cjs.map +1 -1
  55. package/dist/cjs/session/handlers.cjs +18 -9
  56. package/dist/cjs/session/handlers.cjs.map +1 -1
  57. package/dist/cjs/stream.cjs +35 -10
  58. package/dist/cjs/stream.cjs.map +1 -1
  59. package/dist/cjs/summarization/node.cjs +28 -14
  60. package/dist/cjs/summarization/node.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +56 -63
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  64. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  65. package/dist/cjs/tools/search/tool.cjs +9 -1
  66. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  67. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  68. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  69. package/dist/cjs/utils/index.cjs +1 -1
  70. package/dist/cjs/utils/llm.cjs +1 -1
  71. package/dist/cjs/utils/llm.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +307 -20
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/cjs/utils/toolContent.cjs +1514 -0
  75. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  76. package/dist/esm/agents/AgentContext.mjs +2 -1
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/constants.mjs +19 -1
  79. package/dist/esm/common/constants.mjs.map +1 -1
  80. package/dist/esm/events.mjs +10 -1
  81. package/dist/esm/events.mjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +798 -116
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/index.mjs +12 -1
  89. package/dist/esm/hooks/index.mjs.map +1 -1
  90. package/dist/esm/hooks/types.mjs +1 -0
  91. package/dist/esm/hooks/types.mjs.map +1 -1
  92. package/dist/esm/langfuse.mjs +9 -1
  93. package/dist/esm/langfuse.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/google/utils/common.mjs +19 -7
  99. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  100. package/dist/esm/llm/invoke.mjs +332 -8
  101. package/dist/esm/llm/invoke.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +41 -5
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +4 -1
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/preempt.mjs +131 -0
  109. package/dist/esm/llm/preempt.mjs.map +1 -0
  110. package/dist/esm/main.mjs +14 -11
  111. package/dist/esm/messages/alternation.mjs +91 -0
  112. package/dist/esm/messages/alternation.mjs.map +1 -0
  113. package/dist/esm/messages/cache.mjs +55 -34
  114. package/dist/esm/messages/cache.mjs.map +1 -1
  115. package/dist/esm/messages/contextPruning.mjs +17 -43
  116. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  117. package/dist/esm/messages/core.mjs +305 -24
  118. package/dist/esm/messages/core.mjs.map +1 -1
  119. package/dist/esm/messages/format.mjs +290 -61
  120. package/dist/esm/messages/format.mjs.map +1 -1
  121. package/dist/esm/messages/handoffCue.mjs +61 -0
  122. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  123. package/dist/esm/messages/index.mjs +4 -1
  124. package/dist/esm/messages/injected.mjs +60 -0
  125. package/dist/esm/messages/injected.mjs.map +1 -0
  126. package/dist/esm/messages/prune.mjs +500 -128
  127. package/dist/esm/messages/prune.mjs.map +1 -1
  128. package/dist/esm/run.mjs +80 -7
  129. package/dist/esm/run.mjs.map +1 -1
  130. package/dist/esm/session/handlers.mjs +19 -10
  131. package/dist/esm/session/handlers.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +34 -11
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +28 -14
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/ToolNode.mjs +56 -63
  137. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  138. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  139. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  140. package/dist/esm/tools/search/tool.mjs +9 -1
  141. package/dist/esm/tools/search/tool.mjs.map +1 -1
  142. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  143. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  144. package/dist/esm/utils/index.mjs +1 -1
  145. package/dist/esm/utils/llm.mjs +1 -1
  146. package/dist/esm/utils/llm.mjs.map +1 -1
  147. package/dist/esm/utils/tokens.mjs +307 -21
  148. package/dist/esm/utils/tokens.mjs.map +1 -1
  149. package/dist/esm/utils/toolContent.mjs +1503 -0
  150. package/dist/esm/utils/toolContent.mjs.map +1 -0
  151. package/dist/types/common/constants.d.ts +18 -0
  152. package/dist/types/graphs/Graph.d.ts +139 -2
  153. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  154. package/dist/types/hooks/index.d.ts +12 -1
  155. package/dist/types/hooks/types.d.ts +45 -6
  156. package/dist/types/index.d.ts +1 -1
  157. package/dist/types/llm/invoke.d.ts +34 -1
  158. package/dist/types/llm/openai/index.d.ts +4 -0
  159. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  160. package/dist/types/llm/openrouter/index.d.ts +5 -0
  161. package/dist/types/llm/preempt.d.ts +40 -0
  162. package/dist/types/messages/alternation.d.ts +23 -0
  163. package/dist/types/messages/contextPruning.d.ts +2 -1
  164. package/dist/types/messages/core.d.ts +36 -0
  165. package/dist/types/messages/format.d.ts +6 -0
  166. package/dist/types/messages/handoffCue.d.ts +40 -0
  167. package/dist/types/messages/index.d.ts +3 -0
  168. package/dist/types/messages/injected.d.ts +3 -0
  169. package/dist/types/messages/prune.d.ts +16 -12
  170. package/dist/types/run.d.ts +7 -0
  171. package/dist/types/stream.d.ts +14 -0
  172. package/dist/types/tools/ToolNode.d.ts +0 -7
  173. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  174. package/dist/types/tools/search/types.d.ts +31 -2
  175. package/dist/types/types/graph.d.ts +8 -1
  176. package/dist/types/types/run.d.ts +65 -0
  177. package/dist/types/types/stream.d.ts +1 -26
  178. package/dist/types/utils/tokens.d.ts +7 -0
  179. package/dist/types/utils/toolContent.d.ts +107 -0
  180. package/package.json +1 -1
  181. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  182. package/src/agents/AgentContext.ts +1 -0
  183. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  184. package/src/common/constants.ts +21 -0
  185. package/src/events.ts +15 -1
  186. package/src/graphs/Graph.ts +1261 -198
  187. package/src/graphs/MultiAgentGraph.ts +6 -1
  188. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
  189. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  190. package/src/hooks/HookRegistry.ts +40 -0
  191. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  192. package/src/hooks/index.ts +16 -2
  193. package/src/hooks/types.ts +47 -3
  194. package/src/index.ts +1 -1
  195. package/src/langfuse.ts +26 -1
  196. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  197. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  198. package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
  199. package/src/llm/bedrock/utils/message_inputs.ts +100 -24
  200. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  201. package/src/llm/google/utils/common.test.ts +75 -9
  202. package/src/llm/google/utils/common.ts +34 -3
  203. package/src/llm/invoke.alternation.test.ts +87 -0
  204. package/src/llm/invoke.handoffCue.test.ts +112 -0
  205. package/src/llm/invoke.test.ts +488 -0
  206. package/src/llm/invoke.ts +611 -8
  207. package/src/llm/openai/index.ts +106 -6
  208. package/src/llm/openai/utils/index.ts +109 -53
  209. package/src/llm/openai/utils/messages.test.ts +330 -1
  210. package/src/llm/openrouter/index.ts +12 -2
  211. package/src/llm/openrouter/reasoning.test.ts +314 -0
  212. package/src/llm/preempt.test.ts +323 -0
  213. package/src/llm/preempt.ts +178 -0
  214. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  215. package/src/messages/alternation.test.ts +212 -0
  216. package/src/messages/alternation.ts +112 -0
  217. package/src/messages/cache.tail.test.ts +193 -0
  218. package/src/messages/cache.test.ts +113 -0
  219. package/src/messages/cache.ts +92 -60
  220. package/src/messages/contextPruning.test.ts +184 -0
  221. package/src/messages/contextPruning.ts +49 -42
  222. package/src/messages/core.ts +653 -40
  223. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  224. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  225. package/src/messages/format.ts +454 -91
  226. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  227. package/src/messages/formatAgentMessages.test.ts +604 -14
  228. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  229. package/src/messages/handoffCue.test.ts +96 -0
  230. package/src/messages/handoffCue.ts +78 -0
  231. package/src/messages/index.ts +3 -0
  232. package/src/messages/injected.test.ts +90 -0
  233. package/src/messages/injected.ts +74 -0
  234. package/src/messages/prune.ts +996 -183
  235. package/src/run.ts +91 -6
  236. package/src/scripts/preempt-probe.ts +330 -0
  237. package/src/scripts/preempt-scenarios.ts +388 -0
  238. package/src/session/handlers.ts +32 -12
  239. package/src/specs/handoffCue.test.ts +165 -0
  240. package/src/specs/langfuse-callbacks.test.ts +352 -2
  241. package/src/specs/preemptSeal.test.ts +309 -0
  242. package/src/specs/prune.test.ts +1083 -6
  243. package/src/specs/summarization-unit.test.ts +105 -0
  244. package/src/specs/tokens.test.ts +609 -32
  245. package/src/stream.dispatch.test.ts +63 -0
  246. package/src/stream.ts +59 -23
  247. package/src/summarization/__tests__/node.test.ts +77 -0
  248. package/src/summarization/node.ts +46 -18
  249. package/src/tools/ToolNode.ts +150 -99
  250. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  251. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  252. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  253. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  254. package/src/tools/search/keenable-scraper.test.ts +153 -0
  255. package/src/tools/search/keenable-scraper.ts +137 -0
  256. package/src/tools/search/tool.ts +13 -2
  257. package/src/tools/search/types.ts +50 -3
  258. package/src/tools/toolOutputReferences.ts +6 -0
  259. package/src/types/graph.ts +12 -1
  260. package/src/types/run.ts +67 -0
  261. package/src/types/stream.ts +1 -42
  262. package/src/utils/llm.test.ts +18 -0
  263. package/src/utils/llm.ts +4 -1
  264. package/src/utils/tokens.ts +586 -31
  265. package/src/utils/toolContent.test.ts +1432 -0
  266. package/src/utils/toolContent.ts +2707 -0
  267. package/dist/cjs/splitStream.cjs +0 -151
  268. package/dist/cjs/splitStream.cjs.map +0 -1
  269. package/dist/esm/splitStream.mjs +0 -150
  270. package/dist/esm/splitStream.mjs.map +0 -1
  271. package/dist/types/mockStream.d.ts +0 -32
  272. package/dist/types/splitStream.d.ts +0 -37
  273. package/src/mockStream.ts +0 -99
  274. package/src/splitStream.ts +0 -234
@@ -0,0 +1,137 @@
1
+ import axios from 'axios';
2
+ import type * as t from './types';
3
+ import { createDefaultLogger } from './utils';
4
+
5
+ const DEFAULT_KEENABLE_SCRAPE_TIMEOUT = 15000;
6
+
7
+ /** Keyed and keyless fetch endpoints. Keenable reads any URL as clean markdown
8
+ * without a key via the public endpoint; a key only lifts rate limits. */
9
+ const KEENABLE_FETCH_API_URL = 'https://api.keenable.ai/v1/fetch';
10
+ const KEENABLE_FETCH_PUBLIC_URL = 'https://api.keenable.ai/v1/fetch/public';
11
+
12
+ export class KeenableScraper implements t.BaseScraper {
13
+ private apiKey: string | undefined;
14
+ private apiUrl: string;
15
+ private timeout: number;
16
+ private attributionTitle: string;
17
+ private logger: t.Logger;
18
+
19
+ constructor(config: t.KeenableScraperConfig = {}) {
20
+ const resolvedKey = config.apiKey ?? process.env.KEENABLE_API_KEY;
21
+ this.apiKey =
22
+ resolvedKey != null && resolvedKey !== '' ? resolvedKey : undefined;
23
+ this.apiUrl =
24
+ config.apiUrl ??
25
+ process.env.KEENABLE_FETCH_URL ??
26
+ (this.apiKey != null
27
+ ? KEENABLE_FETCH_API_URL
28
+ : KEENABLE_FETCH_PUBLIC_URL);
29
+ this.timeout = config.timeout ?? DEFAULT_KEENABLE_SCRAPE_TIMEOUT;
30
+ this.attributionTitle = config.attributionTitle ?? 'LibreChat';
31
+ this.logger = config.logger || createDefaultLogger();
32
+ }
33
+
34
+ private buildHeaders(): Record<string, string> {
35
+ /** X-Keenable-Title is used for traffic attribution and required on the
36
+ * keyless endpoint; the key only lifts rate limits, so it is sent only when
37
+ * present. */
38
+ const headers: Record<string, string> = {
39
+ 'X-Keenable-Title': this.attributionTitle,
40
+ };
41
+ if (this.apiKey != null) {
42
+ headers['X-API-Key'] = this.apiKey;
43
+ }
44
+ return headers;
45
+ }
46
+
47
+ async scrapeUrl(
48
+ url: string,
49
+ options: t.KeenableScrapeOptions = {}
50
+ ): Promise<[string, t.KeenableScrapeResponse]> {
51
+ if (!url || !url.trim()) {
52
+ return [url, { success: false, error: 'URL cannot be empty' }];
53
+ }
54
+
55
+ try {
56
+ const response = await axios.get<t.KeenableFetchResult>(this.apiUrl, {
57
+ params: { url },
58
+ headers: this.buildHeaders(),
59
+ timeout: options.timeout ?? this.timeout,
60
+ });
61
+
62
+ const data = response.data;
63
+ const content = data.content ?? '';
64
+ if (!content) {
65
+ return [
66
+ url,
67
+ { success: false, error: 'Keenable Fetch returned no content' },
68
+ ];
69
+ }
70
+
71
+ return [
72
+ url,
73
+ {
74
+ success: true,
75
+ data: {
76
+ content,
77
+ title: data.title,
78
+ description: data.description,
79
+ url: data.url ?? url,
80
+ },
81
+ },
82
+ ];
83
+ } catch (error) {
84
+ const errorMessage =
85
+ error instanceof Error ? error.message : String(error);
86
+ return [
87
+ url,
88
+ {
89
+ success: false,
90
+ error: `Keenable Fetch API request failed: ${errorMessage}`,
91
+ },
92
+ ];
93
+ }
94
+ }
95
+
96
+ async scrapeUrls(
97
+ urls: string[],
98
+ options: t.KeenableScrapeOptions = {}
99
+ ): Promise<Array<[string, t.KeenableScrapeResponse]>> {
100
+ /** Keenable fetch is single-URL; run the batch concurrently. */
101
+ return Promise.all(urls.map((url) => this.scrapeUrl(url, options)));
102
+ }
103
+
104
+ extractContent(
105
+ response: t.KeenableScrapeResponse
106
+ ): [string, undefined | t.References] {
107
+ if (!response.success || !response.data) {
108
+ return ['', undefined];
109
+ }
110
+ /** Keenable returns clean markdown with no separate media arrays, so there
111
+ * are no structured references to surface. */
112
+ return [response.data.content, undefined];
113
+ }
114
+
115
+ extractMetadata(response: t.KeenableScrapeResponse): t.GenericScrapeMetadata {
116
+ if (!response.success || !response.data) {
117
+ return {};
118
+ }
119
+ const metadata: t.GenericScrapeMetadata = {};
120
+ if (response.data.title != null) {
121
+ metadata.title = response.data.title;
122
+ }
123
+ if (response.data.description != null && response.data.description !== '') {
124
+ metadata.description = response.data.description;
125
+ }
126
+ if (response.data.url != null) {
127
+ metadata.url = response.data.url;
128
+ }
129
+ return metadata;
130
+ }
131
+ }
132
+
133
+ export const createKeenableScraper = (
134
+ config: t.KeenableScraperConfig = {}
135
+ ): KeenableScraper => {
136
+ return new KeenableScraper(config);
137
+ };
@@ -13,6 +13,7 @@ import {
13
13
  DATE_RANGE,
14
14
  } from './schema';
15
15
  import { createSearchAPI, createSourceProcessor } from './search';
16
+ import { createKeenableScraper } from './keenable-scraper';
16
17
  import { createSerperScraper } from './serper-scraper';
17
18
  import { createTavilyScraper } from './tavily-scraper';
18
19
  import { createFirecrawlScraper } from './firecrawl';
@@ -378,6 +379,7 @@ export const createSearchTool = (
378
379
  keenableApiKey,
379
380
  keenableApiUrl,
380
381
  keenableSearchOptions,
382
+ keenableScraperOptions,
381
383
  rerankerType = 'cohere',
382
384
  rerankerTimeout,
383
385
  topResults = 5,
@@ -476,13 +478,22 @@ export const createSearchTool = (
476
478
  } else if (scraperProvider === 'crw') {
477
479
  scraperInstance = createCrwScraper({
478
480
  ...crwScraperOptions,
479
- apiKey:
480
- crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
481
+ apiKey: crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
481
482
  apiUrl: crwScraperOptions?.apiUrl ?? crwApiUrl,
482
483
  timeout: scraperTimeout ?? crwScraperOptions?.timeout,
483
484
  formats: crwScraperOptions?.formats ?? ['markdown', 'rawHtml'],
484
485
  logger,
485
486
  });
487
+ } else if (scraperProvider === 'keenable') {
488
+ scraperInstance = createKeenableScraper({
489
+ ...keenableScraperOptions,
490
+ apiKey: keenableScraperOptions?.apiKey ?? keenableApiKey,
491
+ timeout: scraperTimeout ?? keenableScraperOptions?.timeout,
492
+ attributionTitle:
493
+ keenableScraperOptions?.attributionTitle ??
494
+ keenableSearchOptions?.attributionTitle,
495
+ logger,
496
+ });
486
497
  } else {
487
498
  scraperInstance = createFirecrawlScraper({
488
499
  ...firecrawlOptions,
@@ -3,8 +3,18 @@ import type { Logger as WinstonLogger } from 'winston';
3
3
  import type { BaseReranker } from './rerankers';
4
4
  import { DATE_RANGE } from './schema';
5
5
 
6
- export type SearchProvider = 'serper' | 'searxng' | 'tavily' | 'keenable' | 'crw';
7
- export type ScraperProvider = 'firecrawl' | 'serper' | 'tavily' | 'crw';
6
+ export type SearchProvider =
7
+ | 'serper'
8
+ | 'searxng'
9
+ | 'tavily'
10
+ | 'keenable'
11
+ | 'crw';
12
+ export type ScraperProvider =
13
+ | 'firecrawl'
14
+ | 'serper'
15
+ | 'tavily'
16
+ | 'crw'
17
+ | 'keenable';
8
18
  export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
9
19
 
10
20
  export interface Highlight {
@@ -198,6 +208,41 @@ export interface KeenableSearchResponse {
198
208
  results?: KeenableSearchResult[];
199
209
  }
200
210
 
211
+ export interface KeenableScraperConfig {
212
+ apiKey?: string;
213
+ /** Override the fetch endpoint base (default: public keyless, keyed when a
214
+ * key is set). Env fallback: KEENABLE_FETCH_URL. */
215
+ apiUrl?: string;
216
+ timeout?: number;
217
+ logger?: Logger;
218
+ /** Sent as the X-Keenable-Title attribution header. Defaults to "LibreChat". */
219
+ attributionTitle?: string;
220
+ }
221
+
222
+ export type KeenableScrapeOptions = Omit<
223
+ KeenableScraperConfig,
224
+ 'apiKey' | 'apiUrl' | 'logger'
225
+ >;
226
+
227
+ /** Raw JSON shape returned by GET /v1/fetch{,/public}?url=... */
228
+ export interface KeenableFetchResult {
229
+ url?: string;
230
+ title?: string;
231
+ content?: string;
232
+ description?: string;
233
+ }
234
+
235
+ export interface KeenableScrapeResponse {
236
+ success: boolean;
237
+ data?: {
238
+ content: string;
239
+ title?: string;
240
+ description?: string;
241
+ url?: string;
242
+ };
243
+ error?: string;
244
+ }
245
+
201
246
  export type References = {
202
247
  links: MediaReference[];
203
248
  images: MediaReference[];
@@ -311,6 +356,7 @@ export interface SearchToolConfig
311
356
  FirecrawlConfig {
312
357
  tavilyScraperOptions?: TavilyScraperConfig;
313
358
  crwScraperOptions?: CrwScraperConfig;
359
+ keenableScraperOptions?: KeenableScraperConfig;
314
360
  /** Max chars of highlight content this tool feeds the MODEL per search (the
315
361
  * dominant, otherwise-unbounded part of the output). Distinct from
316
362
  * `maxContentLength`, which caps scraped/reranked content per source — full
@@ -351,7 +397,8 @@ export type AnyScraperResponse =
351
397
  | FirecrawlScrapeResponse
352
398
  | SerperScrapeResponse
353
399
  | TavilyScrapeResponse
354
- | CrwScrapeResponse;
400
+ | CrwScrapeResponse
401
+ | KeenableScrapeResponse;
355
402
 
356
403
  /** Base Scraper Interface */
357
404
  export interface BaseScraper {
@@ -29,6 +29,7 @@ import {
29
29
  HARD_MAX_TOOL_RESULT_CHARS,
30
30
  HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
31
31
  } from '@/utils/truncation';
32
+ import { isComputerCallOutputMessage } from '@/utils/toolContent';
32
33
 
33
34
  /**
34
35
  * Non-global matcher for a single `{{tool<i>turn<n>}}` placeholder.
@@ -656,6 +657,11 @@ export function annotateMessagesForLLM(
656
657
  const hasRefScope = '_refScope' in meta;
657
658
  const hasUnresolvedField = '_unresolvedRefs' in meta;
658
659
  if (!hasRefKey && !hasRefScope && !hasUnresolvedField) continue;
660
+ if (isComputerCallOutputMessage(m)) {
661
+ out ??= messages.slice();
662
+ out[i] = cloneToolMessageWithContent(m as ToolMessage, m.content);
663
+ continue;
664
+ }
659
665
 
660
666
  const refKey = readRefKey(meta);
661
667
  const unresolved = readUnresolvedRefs(meta);
@@ -30,7 +30,11 @@ import type {
30
30
  MessageDeltaEvent,
31
31
  ReasoningDeltaEvent,
32
32
  } from '@/types/stream';
33
- import type { TokenCounter, TokenBudgetBreakdown } from '@/types/run';
33
+ import type {
34
+ TokenCounter,
35
+ StreamPreemption,
36
+ TokenBudgetBreakdown,
37
+ } from '@/types/run';
34
38
  import type { Providers, Callback, GraphNodeKeys } from '@/common';
35
39
  import type { StandardGraph, MultiAgentGraph } from '@/graphs';
36
40
  import type { ClientOptions } from '@/types/llm';
@@ -346,6 +350,13 @@ export type StandardGraphInput = {
346
350
  * hook inputs carry only `executingAgentId`.
347
351
  */
348
352
  subagentScope?: boolean;
353
+ /**
354
+ * Cooperative preemption, forwarded from `RunConfig.preemption`. Only ever
355
+ * set on the top-level graph: a steer targets the conversation, so subagent
356
+ * children must run to completion and `buildChildInputs` does not propagate
357
+ * this field.
358
+ */
359
+ preemption?: StreamPreemption;
349
360
  };
350
361
 
351
362
  export type GraphEdge = {
package/src/types/run.ts CHANGED
@@ -115,6 +115,65 @@ export type StandardGraphConfig = Omit<
115
115
  'edges' | 'type'
116
116
  > & { type?: 'standard'; signal?: AbortSignal };
117
117
 
118
+ /**
119
+ * Cooperative mid-generation preemption. Lets a host seal the live model
120
+ * stream at the next provider-safe token boundary — the run is never
121
+ * aborted, the partial assistant turn is kept, and the graph self-loops
122
+ * into a fresh model call once the `PreemptBoundary` hook has injected
123
+ * whatever the host queued.
124
+ *
125
+ * Preconditions the host MUST satisfy:
126
+ * - `shouldPreempt` is polled once per streamed chunk on the top-level
127
+ * graph. It must be synchronous, allocation-free and O(1) — never I/O.
128
+ * It must also be LEVEL-TRIGGERED (non-consuming): the SDK never clears
129
+ * the host's request, and a true result is only honored once the
130
+ * accumulated chunk is provider-safe, so the predicate may be polled
131
+ * many times before a seal. A one-shot read that clears its own pending
132
+ * flag would silently lose the request on an unsafe chunk (leading
133
+ * whitespace/reasoning, an in-flight tool call) — keep returning true
134
+ * until the `PreemptBoundary` drain hands over the queued injection,
135
+ * then disarm there.
136
+ * - Sealing is only honored on the SDK's own dispatch loop. A run whose
137
+ * registered `CHAT_MODEL_STREAM` handler IS the SDK dispatcher — or wraps
138
+ * it, which `composeEventHandlers` and `createRunHandlers` both do —
139
+ * consumes chunks through a decoupled `streamEvents` reader that can lag
140
+ * the accumulated chunk, so those runs never seal.
141
+ *
142
+ * Detection is by capability, not identity: the dispatcher carries
143
+ * `SDK_STREAM_DISPATCH` and the SDK's wrappers propagate it. A handler
144
+ * that merely OBSERVES the raw chunk echo — LibreChat's no-op
145
+ * `OpenAIChatModelStreamHandler`, say — is unbranded and does NOT disable
146
+ * sealing, because it assigns no content-part indices and so cannot be
147
+ * inverted. A host that renders from the raw feed and wants the opt-out
148
+ * should brand its handler with `SDK_STREAM_DISPATCH`.
149
+ * - `RunConfig.tokenCounter` should be set. A sealed turn ends before most
150
+ * providers send their usage chunk, so the synthetic `CHAT_MODEL_END`
151
+ * falls back to the counter to report `output_tokens`. Without one that
152
+ * fallback silently no-ops and the sealed turn's usage is lost — verified
153
+ * live: OpenAI, Azure OpenAI and DeepSeek report no usage for the sealed
154
+ * segment without a counter, while Anthropic streams usage incrementally
155
+ * and reports it either way.
156
+ */
157
+ export interface StreamPreemption {
158
+ /**
159
+ * Polled once per streamed chunk. Synchronous, allocation-free, O(1), and
160
+ * level-triggered — keep returning true until the `PreemptBoundary` drain
161
+ * consumes the request; a self-clearing read loses it on an unsafe chunk.
162
+ */
163
+ shouldPreempt: () => boolean;
164
+ /**
165
+ * Max cooperative seals per run. Each seal costs one extra superstep, so
166
+ * this also bounds the recursion-limit headroom the run reserves.
167
+ */
168
+ maxSeals?: number;
169
+ }
170
+
171
+ /** Seals honored and boundaries that had nothing to inject, per run. */
172
+ export type PreemptStats = {
173
+ seals: number;
174
+ emptyBoundaries: number;
175
+ };
176
+
118
177
  export type RunConfig = {
119
178
  runId: string;
120
179
  graphConfig: LegacyGraphConfig | StandardGraphConfig | MultiAgentGraphConfig;
@@ -146,6 +205,14 @@ export type RunConfig = {
146
205
  * block to prevent leaks.
147
206
  */
148
207
  hooks?: HookRegistry;
208
+ /**
209
+ * Opt-in cooperative preemption for this run. Requires a `hooks` registry
210
+ * with a `PreemptBoundary` matcher — the seal only stops the stream, the
211
+ * hook is what supplies the messages to resume with. Omit to keep the
212
+ * pre-preemption behavior, where a mid-run injection can only land at a
213
+ * tool boundary.
214
+ */
215
+ preemption?: StreamPreemption;
149
216
  returnContent?: boolean;
150
217
  tokenCounter?: TokenCounter;
151
218
  indexTokenCountMap?: Record<string, number>;
@@ -8,7 +8,6 @@ import type {
8
8
  import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
9
9
  import type { LLMResult, Generation } from '@langchain/core/outputs';
10
10
  import type { Command } from '@langchain/langgraph';
11
- import type OpenAITypes from 'openai';
12
11
  import type { AnthropicContentBlock } from '@/llm/anthropic/types';
13
12
  import type { SummarizeCompleteEvent } from '@/types/summarize';
14
13
  import type { ToolEndEvent } from '@/types/tools';
@@ -336,7 +335,7 @@ export type ToolCallPart = {
336
335
  /** If provided, an identifier associated with the tool call */
337
336
  id?: string;
338
337
  /** If provided, the output of the tool call */
339
- output?: string;
338
+ output?: ToolResultContent['content'];
340
339
  /** Auth URL */
341
340
  auth?: string;
342
341
  /** Expiration time */
@@ -393,46 +392,6 @@ export interface TMessage {
393
392
 
394
393
  export type TPayload = Array<Partial<TMessage>>;
395
394
 
396
- export type CustomChunkDelta =
397
- | null
398
- | undefined
399
- | (Partial<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice.Delta> & {
400
- reasoning?: string | null;
401
- reasoning_content?: string | null;
402
- });
403
- export type CustomChunkChoice = Partial<
404
- Omit<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice, 'delta'> & {
405
- delta?: CustomChunkDelta;
406
- }
407
- >;
408
- export type CustomChunk = Partial<OpenAITypes.ChatCompletionChunk> & {
409
- choices?: Partial<Array<CustomChunkChoice>>;
410
- };
411
-
412
- export type SplitStreamHandlers = Partial<{
413
- [GraphEvents.ON_RUN_STEP]: ({
414
- event,
415
- data,
416
- }: {
417
- event: GraphEvents;
418
- data: RunStep;
419
- }) => void;
420
- [GraphEvents.ON_MESSAGE_DELTA]: ({
421
- event,
422
- data,
423
- }: {
424
- event: GraphEvents;
425
- data: MessageDeltaEvent;
426
- }) => void;
427
- [GraphEvents.ON_REASONING_DELTA]: ({
428
- event,
429
- data,
430
- }: {
431
- event: GraphEvents;
432
- data: ReasoningDeltaEvent;
433
- }) => void;
434
- }>;
435
-
436
395
  export type SummarizeDeltaData = {
437
396
  id: string;
438
397
  delta: {
@@ -0,0 +1,18 @@
1
+ import { isAnthropicLike } from './llm';
2
+ import { Providers } from '@/common';
3
+
4
+ describe('isAnthropicLike', () => {
5
+ it('treats the default Bedrock model as Claude', () => {
6
+ expect(isAnthropicLike(Providers.BEDROCK)).toBe(true);
7
+ expect(
8
+ isAnthropicLike(Providers.BEDROCK, {
9
+ model: 'anthropic.claude-sonnet-4-5',
10
+ })
11
+ ).toBe(true);
12
+ expect(
13
+ isAnthropicLike(Providers.BEDROCK, {
14
+ model: 'amazon.nova-pro-v1:0',
15
+ })
16
+ ).toBe(false);
17
+ });
18
+ });
package/src/utils/llm.ts CHANGED
@@ -32,7 +32,10 @@ export function isAnthropicLike(
32
32
  ): boolean {
33
33
  if (provider === Providers.ANTHROPIC) return true;
34
34
  if (provider === Providers.BEDROCK) {
35
- return /claude/i.test(String(clientOptions?.model ?? ''));
35
+ return (
36
+ clientOptions?.model == null ||
37
+ /claude/i.test(String(clientOptions.model))
38
+ );
36
39
  }
37
40
  return false;
38
41
  }