@librechat/agents 3.3.4 → 3.3.6

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 (263) hide show
  1. package/dist/cjs/common/constants.cjs +21 -0
  2. package/dist/cjs/common/constants.cjs.map +1 -1
  3. package/dist/cjs/events.cjs +10 -1
  4. package/dist/cjs/events.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +456 -7
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  10. package/dist/cjs/hooks/index.cjs +12 -0
  11. package/dist/cjs/hooks/index.cjs.map +1 -1
  12. package/dist/cjs/hooks/types.cjs +1 -0
  13. package/dist/cjs/hooks/types.cjs.map +1 -1
  14. package/dist/cjs/langfuse.cjs +8 -0
  15. package/dist/cjs/langfuse.cjs.map +1 -1
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +1 -3
  17. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  18. package/dist/cjs/llm/invoke.cjs +268 -3
  19. package/dist/cjs/llm/invoke.cjs.map +1 -1
  20. package/dist/cjs/llm/openai/index.cjs +51 -6
  21. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  22. package/dist/cjs/llm/preempt.cjs +132 -0
  23. package/dist/cjs/llm/preempt.cjs.map +1 -0
  24. package/dist/cjs/main.cjs +31 -3
  25. package/dist/cjs/messages/alternation.cjs +92 -0
  26. package/dist/cjs/messages/alternation.cjs.map +1 -0
  27. package/dist/cjs/messages/format.cjs +72 -0
  28. package/dist/cjs/messages/format.cjs.map +1 -1
  29. package/dist/cjs/messages/handoffCue.cjs +63 -0
  30. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  31. package/dist/cjs/messages/index.cjs +3 -0
  32. package/dist/cjs/messages/injected.cjs +60 -0
  33. package/dist/cjs/messages/injected.cjs.map +1 -0
  34. package/dist/cjs/run.cjs +80 -7
  35. package/dist/cjs/run.cjs.map +1 -1
  36. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  37. package/dist/cjs/session/handlers.cjs +18 -9
  38. package/dist/cjs/session/handlers.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +34 -8
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/tools/BashExecutor.cjs +3 -1
  42. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  43. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -0
  44. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  45. package/dist/cjs/tools/CodeExecutor.cjs +19 -16
  46. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  47. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +2 -0
  48. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  49. package/dist/cjs/tools/ReadFile.cjs +8 -4
  50. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  51. package/dist/cjs/tools/SkillTool.cjs +2 -0
  52. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  53. package/dist/cjs/tools/SubagentTool.cjs +3 -0
  54. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  55. package/dist/cjs/tools/ToolNode.cjs +91 -50
  56. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  57. package/dist/cjs/tools/ToolSearch.cjs +5 -1
  58. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  59. package/dist/cjs/tools/intentArg.cjs +230 -0
  60. package/dist/cjs/tools/intentArg.cjs.map +1 -0
  61. package/dist/cjs/tools/local/CompileCheckTool.cjs +3 -2
  62. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  63. package/dist/cjs/tools/local/LocalCodingTools.cjs +13 -12
  64. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  65. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +2 -2
  66. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  67. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  68. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  69. package/dist/cjs/tools/search/schema.cjs +2 -0
  70. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  71. package/dist/cjs/tools/search/tool.cjs +40 -2
  72. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  73. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  74. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  75. package/dist/cjs/tools/toolOutputReferences.cjs +41 -7
  76. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  77. package/dist/esm/common/constants.mjs +19 -1
  78. package/dist/esm/common/constants.mjs.map +1 -1
  79. package/dist/esm/events.mjs +10 -1
  80. package/dist/esm/events.mjs.map +1 -1
  81. package/dist/esm/graphs/Graph.mjs +455 -6
  82. package/dist/esm/graphs/Graph.mjs.map +1 -1
  83. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  84. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  85. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  86. package/dist/esm/hooks/index.mjs +12 -1
  87. package/dist/esm/hooks/index.mjs.map +1 -1
  88. package/dist/esm/hooks/types.mjs +1 -0
  89. package/dist/esm/hooks/types.mjs.map +1 -1
  90. package/dist/esm/langfuse.mjs +9 -1
  91. package/dist/esm/langfuse.mjs.map +1 -1
  92. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +1 -3
  93. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  94. package/dist/esm/llm/invoke.mjs +270 -6
  95. package/dist/esm/llm/invoke.mjs.map +1 -1
  96. package/dist/esm/llm/openai/index.mjs +51 -6
  97. package/dist/esm/llm/openai/index.mjs.map +1 -1
  98. package/dist/esm/llm/preempt.mjs +131 -0
  99. package/dist/esm/llm/preempt.mjs.map +1 -0
  100. package/dist/esm/main.mjs +11 -7
  101. package/dist/esm/messages/alternation.mjs +91 -0
  102. package/dist/esm/messages/alternation.mjs.map +1 -0
  103. package/dist/esm/messages/format.mjs +72 -0
  104. package/dist/esm/messages/format.mjs.map +1 -1
  105. package/dist/esm/messages/handoffCue.mjs +61 -0
  106. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  107. package/dist/esm/messages/index.mjs +3 -0
  108. package/dist/esm/messages/injected.mjs +60 -0
  109. package/dist/esm/messages/injected.mjs.map +1 -0
  110. package/dist/esm/run.mjs +80 -7
  111. package/dist/esm/run.mjs.map +1 -1
  112. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  113. package/dist/esm/session/handlers.mjs +19 -10
  114. package/dist/esm/session/handlers.mjs.map +1 -1
  115. package/dist/esm/stream.mjs +33 -9
  116. package/dist/esm/stream.mjs.map +1 -1
  117. package/dist/esm/tools/BashExecutor.mjs +3 -1
  118. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  119. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -0
  120. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  121. package/dist/esm/tools/CodeExecutor.mjs +19 -16
  122. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  123. package/dist/esm/tools/ProgrammaticToolCalling.mjs +2 -0
  124. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  125. package/dist/esm/tools/ReadFile.mjs +8 -4
  126. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  127. package/dist/esm/tools/SkillTool.mjs +2 -0
  128. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  129. package/dist/esm/tools/SubagentTool.mjs +3 -0
  130. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  131. package/dist/esm/tools/ToolNode.mjs +91 -51
  132. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  133. package/dist/esm/tools/ToolSearch.mjs +5 -1
  134. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  135. package/dist/esm/tools/intentArg.mjs +220 -0
  136. package/dist/esm/tools/intentArg.mjs.map +1 -0
  137. package/dist/esm/tools/local/CompileCheckTool.mjs +3 -2
  138. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  139. package/dist/esm/tools/local/LocalCodingTools.mjs +13 -12
  140. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  141. package/dist/esm/tools/local/LocalExecutionEngine.mjs +2 -2
  142. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  143. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  144. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  145. package/dist/esm/tools/search/schema.mjs +2 -0
  146. package/dist/esm/tools/search/schema.mjs.map +1 -1
  147. package/dist/esm/tools/search/tool.mjs +40 -3
  148. package/dist/esm/tools/search/tool.mjs.map +1 -1
  149. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  150. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  151. package/dist/esm/tools/toolOutputReferences.mjs +41 -7
  152. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  153. package/dist/types/common/constants.d.ts +18 -0
  154. package/dist/types/graphs/Graph.d.ts +138 -1
  155. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  156. package/dist/types/hooks/index.d.ts +12 -1
  157. package/dist/types/hooks/types.d.ts +45 -6
  158. package/dist/types/index.d.ts +2 -1
  159. package/dist/types/llm/invoke.d.ts +7 -0
  160. package/dist/types/llm/openai/index.d.ts +0 -11
  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/handoffCue.d.ts +40 -0
  164. package/dist/types/messages/index.d.ts +3 -0
  165. package/dist/types/messages/injected.d.ts +3 -0
  166. package/dist/types/run.d.ts +7 -0
  167. package/dist/types/stream.d.ts +14 -0
  168. package/dist/types/tools/BashExecutor.d.ts +18 -0
  169. package/dist/types/tools/CodeExecutor.d.ts +18 -0
  170. package/dist/types/tools/ReadFile.d.ts +18 -0
  171. package/dist/types/tools/SkillTool.d.ts +18 -0
  172. package/dist/types/tools/SubagentTool.d.ts +9 -0
  173. package/dist/types/tools/ToolNode.d.ts +37 -12
  174. package/dist/types/tools/ToolSearch.d.ts +18 -0
  175. package/dist/types/tools/intentArg.d.ts +119 -0
  176. package/dist/types/tools/ptcTimeout.d.ts +2 -0
  177. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  178. package/dist/types/tools/search/schema.d.ts +18 -0
  179. package/dist/types/tools/search/tool.d.ts +18 -0
  180. package/dist/types/tools/search/types.d.ts +31 -2
  181. package/dist/types/tools/toolOutputReferences.d.ts +12 -2
  182. package/dist/types/types/graph.d.ts +8 -1
  183. package/dist/types/types/run.d.ts +65 -0
  184. package/dist/types/types/stream.d.ts +12 -25
  185. package/dist/types/types/tools.d.ts +27 -2
  186. package/package.json +1 -1
  187. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  188. package/src/common/constants.ts +21 -0
  189. package/src/events.ts +15 -1
  190. package/src/graphs/Graph.ts +568 -3
  191. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
  192. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  193. package/src/hooks/HookRegistry.ts +40 -0
  194. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  195. package/src/hooks/index.ts +16 -2
  196. package/src/hooks/types.ts +47 -3
  197. package/src/index.ts +2 -1
  198. package/src/langfuse.ts +26 -1
  199. package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
  200. package/src/llm/bedrock/utils/message_inputs.ts +14 -11
  201. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  202. package/src/llm/invoke.alternation.test.ts +87 -0
  203. package/src/llm/invoke.handoffCue.test.ts +112 -0
  204. package/src/llm/invoke.ts +417 -8
  205. package/src/llm/openai/index.ts +74 -10
  206. package/src/llm/openai/llm.spec.ts +105 -0
  207. package/src/llm/preempt.test.ts +323 -0
  208. package/src/llm/preempt.ts +178 -0
  209. package/src/messages/alternation.test.ts +212 -0
  210. package/src/messages/alternation.ts +112 -0
  211. package/src/messages/format.ts +91 -0
  212. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  213. package/src/messages/handoffCue.test.ts +96 -0
  214. package/src/messages/handoffCue.ts +78 -0
  215. package/src/messages/index.ts +3 -0
  216. package/src/messages/injected.test.ts +90 -0
  217. package/src/messages/injected.ts +74 -0
  218. package/src/run.ts +91 -6
  219. package/src/scripts/preempt-probe.ts +330 -0
  220. package/src/scripts/preempt-scenarios.ts +388 -0
  221. package/src/session/handlers.ts +32 -12
  222. package/src/specs/handoffCue.test.ts +165 -0
  223. package/src/specs/langfuse-callbacks.test.ts +352 -2
  224. package/src/specs/preemptSeal.test.ts +309 -0
  225. package/src/stream.dispatch.test.ts +63 -0
  226. package/src/stream.ts +48 -9
  227. package/src/tools/BashExecutor.ts +6 -0
  228. package/src/tools/BashProgrammaticToolCalling.ts +2 -0
  229. package/src/tools/CodeExecutor.ts +6 -0
  230. package/src/tools/ProgrammaticToolCalling.ts +2 -0
  231. package/src/tools/ReadFile.ts +2 -0
  232. package/src/tools/SkillTool.ts +2 -0
  233. package/src/tools/SubagentTool.ts +3 -0
  234. package/src/tools/ToolNode.ts +193 -74
  235. package/src/tools/ToolSearch.ts +3 -0
  236. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +174 -0
  237. package/src/tools/__tests__/intentArg.test.ts +281 -0
  238. package/src/tools/__tests__/intentCoverage.test.ts +139 -0
  239. package/src/tools/__tests__/toolOutputReferences.test.ts +83 -0
  240. package/src/tools/intentArg.ts +323 -0
  241. package/src/tools/local/CompileCheckTool.ts +4 -3
  242. package/src/tools/local/LocalCodingTools.ts +14 -13
  243. package/src/tools/ptcTimeout.ts +2 -0
  244. package/src/tools/search/keenable-scraper.test.ts +153 -0
  245. package/src/tools/search/keenable-scraper.ts +137 -0
  246. package/src/tools/search/outcome.test.ts +90 -0
  247. package/src/tools/search/schema.ts +3 -0
  248. package/src/tools/search/tool.ts +60 -3
  249. package/src/tools/search/types.ts +50 -3
  250. package/src/tools/subagent/SubagentExecutor.ts +2 -1
  251. package/src/tools/toolOutputReferences.ts +77 -9
  252. package/src/types/graph.ts +12 -1
  253. package/src/types/run.ts +67 -0
  254. package/src/types/stream.ts +12 -41
  255. package/src/types/tools.ts +24 -2
  256. package/dist/cjs/splitStream.cjs +0 -151
  257. package/dist/cjs/splitStream.cjs.map +0 -1
  258. package/dist/esm/splitStream.mjs +0 -150
  259. package/dist/esm/splitStream.mjs.map +0 -1
  260. package/dist/types/mockStream.d.ts +0 -32
  261. package/dist/types/splitStream.d.ts +0 -37
  262. package/src/mockStream.ts +0 -99
  263. 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
+ };
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import type * as t from './types';
3
+ import { resolveSearchOutcome } from './tool';
4
+
5
+ const data = (partial: Partial<t.SearchResultData>): t.SearchResultData =>
6
+ ({ turn: 0, ...partial }) as t.SearchResultData;
7
+
8
+ describe('resolveSearchOutcome', () => {
9
+ it('authors a FAILURE label when the processor caught an error', () => {
10
+ expect(
11
+ resolveSearchOutcome(
12
+ data({ error: 'provider timeout', organic: [] }),
13
+ 'oauth handling'
14
+ )
15
+ ).toBe('Search failed for "oauth handling"');
16
+ });
17
+
18
+ it('prefers the failure label over any partial results', () => {
19
+ expect(
20
+ resolveSearchOutcome(
21
+ data({
22
+ error: 'partial failure',
23
+ organic: [{ link: 'a' }] as t.SearchResultData['organic'],
24
+ }),
25
+ 'oauth'
26
+ )
27
+ ).toBe('Search failed for "oauth"');
28
+ });
29
+
30
+ it('ignores an empty-string error', () => {
31
+ expect(
32
+ resolveSearchOutcome(
33
+ data({
34
+ error: '',
35
+ organic: [{ link: 'a' }] as t.SearchResultData['organic'],
36
+ }),
37
+ 'oauth'
38
+ )
39
+ ).toBe('Found 1 result for "oauth"');
40
+ });
41
+
42
+ it('counts every rendered collection kind', () => {
43
+ expect(
44
+ resolveSearchOutcome(
45
+ data({
46
+ organic: [
47
+ { link: 'a' },
48
+ { link: 'b' },
49
+ ] as t.SearchResultData['organic'],
50
+ topStories: [{ link: 'c' }] as t.SearchResultData['topStories'],
51
+ images: [{ imageUrl: 'd' }] as t.SearchResultData['images'],
52
+ videos: [{ link: 'e' }] as t.SearchResultData['videos'],
53
+ places: [{ name: 'f' }] as t.SearchResultData['places'],
54
+ peopleAlsoAsk: [
55
+ { question: 'g' },
56
+ ] as t.SearchResultData['peopleAlsoAsk'],
57
+ }),
58
+ 'oauth'
59
+ )
60
+ ).toBe('Found 7 results for "oauth"');
61
+ });
62
+
63
+ it('counts singleton structured results (knowledge graph / answer box)', () => {
64
+ expect(
65
+ resolveSearchOutcome(
66
+ data({
67
+ knowledgeGraph: {
68
+ title: 'OAuth',
69
+ } as t.SearchResultData['knowledgeGraph'],
70
+ }),
71
+ 'oauth'
72
+ )
73
+ ).toBe('Found 1 result for "oauth"');
74
+ expect(
75
+ resolveSearchOutcome(
76
+ data({
77
+ answerBox: { snippet: 'x' } as t.SearchResultData['answerBox'],
78
+ knowledgeGraph: {
79
+ title: 'OAuth',
80
+ } as t.SearchResultData['knowledgeGraph'],
81
+ }),
82
+ 'oauth'
83
+ )
84
+ ).toBe('Found 2 results for "oauth"');
85
+ });
86
+
87
+ it('leaves a genuine zero-result search to the mechanical transform', () => {
88
+ expect(resolveSearchOutcome(data({ organic: [] }), 'oauth')).toBeUndefined();
89
+ });
90
+ });
@@ -1,3 +1,5 @@
1
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
2
+
1
3
  export enum DATE_RANGE {
2
4
  PAST_HOUR = 'h',
3
5
  PAST_24_HOURS = 'd',
@@ -71,6 +73,7 @@ export const newsSchema = {
71
73
  export const WebSearchToolSchema = {
72
74
  type: 'object',
73
75
  properties: {
76
+ intent: { ...INTENT_PROPERTY },
74
77
  query: querySchema,
75
78
  date: dateSchema,
76
79
  country: countrySchema,
@@ -13,9 +13,11 @@ 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';
20
+ import { INTENT_PROPERTY } from '@/tools/intentArg';
19
21
  import { createCrwScraper } from './crw-scraper';
20
22
  import { expandHighlights } from './highlights';
21
23
  import { formatResultsForLLM } from './format';
@@ -23,6 +25,46 @@ import { createDefaultLogger } from './utils';
23
25
  import { createReranker } from './rerankers';
24
26
  import { Constants } from '@/common';
25
27
 
28
+ /**
29
+ * Settled label for a `web_search` call's intent (see `intentArg.ts`).
30
+ *
31
+ * Counts the result kinds `formatResultsForLLM` actually renders —
32
+ * `references` only tracks links embedded in extracted highlights, so it
33
+ * undercounts ordinary results and can overcount when one highlight embeds
34
+ * several links.
35
+ *
36
+ * A caught provider or processing failure is reported through `data.error`
37
+ * while the tool still returns NORMALLY, so that case must author its own
38
+ * label: the `ToolMessage` carries success status, and a bare intent would
39
+ * otherwise settle mechanically from "Searching…" to "Searched…" and present
40
+ * a failed search as a successful one.
41
+ *
42
+ * Returns undefined for a genuine zero-result search, leaving the host's
43
+ * mechanical past-tense transform to label it.
44
+ */
45
+ export function resolveSearchOutcome(
46
+ data: t.SearchResultData,
47
+ query: string
48
+ ): string | undefined {
49
+ if (data.error != null && data.error !== '') {
50
+ return `Search failed for "${query}"`;
51
+ }
52
+ const count =
53
+ (data.organic?.length ?? 0) +
54
+ (data.topStories?.length ?? 0) +
55
+ (data.news?.length ?? 0) +
56
+ (data.images?.length ?? 0) +
57
+ (data.videos?.length ?? 0) +
58
+ (data.places?.length ?? 0) +
59
+ (data.peopleAlsoAsk?.length ?? 0) +
60
+ (data.knowledgeGraph != null ? 1 : 0) +
61
+ (data.answerBox != null ? 1 : 0);
62
+ if (count === 0) {
63
+ return undefined;
64
+ }
65
+ return `Found ${count} result${count === 1 ? '' : 's'} for "${query}"`;
66
+ }
67
+
26
68
  /**
27
69
  * Executes parallel searches and merges the results,
28
70
  * deduplicating top stories by link
@@ -334,7 +376,11 @@ function createTool({
334
376
  maxOutputChars
335
377
  );
336
378
  const data: t.SearchResultData = { turn, ...searchResult, references };
337
- return [output, { [Constants.WEB_SEARCH]: data }];
379
+ const outcome = resolveSearchOutcome(data, query);
380
+ return [
381
+ output,
382
+ { [Constants.WEB_SEARCH]: data, ...(outcome != null && { outcome }) },
383
+ ];
338
384
  },
339
385
  {
340
386
  name: WebSearchToolName,
@@ -378,6 +424,7 @@ export const createSearchTool = (
378
424
  keenableApiKey,
379
425
  keenableApiUrl,
380
426
  keenableSearchOptions,
427
+ keenableScraperOptions,
381
428
  rerankerType = 'cohere',
382
429
  rerankerTimeout,
383
430
  topResults = 5,
@@ -419,6 +466,7 @@ export const createSearchTool = (
419
466
  : tavilySearchOptions;
420
467
 
421
468
  const schemaProperties: Record<string, unknown> = {
469
+ intent: { ...INTENT_PROPERTY },
422
470
  query: querySchema,
423
471
  date: dateSchema,
424
472
  images: imagesSchema,
@@ -476,13 +524,22 @@ export const createSearchTool = (
476
524
  } else if (scraperProvider === 'crw') {
477
525
  scraperInstance = createCrwScraper({
478
526
  ...crwScraperOptions,
479
- apiKey:
480
- crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
527
+ apiKey: crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
481
528
  apiUrl: crwScraperOptions?.apiUrl ?? crwApiUrl,
482
529
  timeout: scraperTimeout ?? crwScraperOptions?.timeout,
483
530
  formats: crwScraperOptions?.formats ?? ['markdown', 'rawHtml'],
484
531
  logger,
485
532
  });
533
+ } else if (scraperProvider === 'keenable') {
534
+ scraperInstance = createKeenableScraper({
535
+ ...keenableScraperOptions,
536
+ apiKey: keenableScraperOptions?.apiKey ?? keenableApiKey,
537
+ timeout: scraperTimeout ?? keenableScraperOptions?.timeout,
538
+ attributionTitle:
539
+ keenableScraperOptions?.attributionTitle ??
540
+ keenableSearchOptions?.attributionTitle,
541
+ logger,
542
+ });
486
543
  } else {
487
544
  scraperInstance = createFirecrawlScraper({
488
545
  ...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 {
@@ -115,7 +115,7 @@ type SanitizedStepCompleted =
115
115
  };
116
116
 
117
117
  type SanitizedProcessedToolCall = Partial<
118
- Pick<ProcessedToolCall, 'args' | 'id' | 'name' | 'output' | 'progress'>
118
+ Pick<ProcessedToolCall, 'args' | 'id' | 'name' | 'output' | 'progress' | 'outcome'>
119
119
  >;
120
120
 
121
121
  type SanitizedRunStepCompleted = {
@@ -1195,6 +1195,7 @@ function sanitizeProcessedToolCall(
1195
1195
  sanitized.args = call.args;
1196
1196
  }
1197
1197
  assignString(sanitized, 'output', call.output);
1198
+ assignString(sanitized, 'outcome', call.outcome);
1198
1199
  assignNumber(sanitized, 'progress', call.progress);
1199
1200
  return sanitized;
1200
1201
  }
@@ -30,6 +30,25 @@ import {
30
30
  HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
31
31
  } from '@/utils/truncation';
32
32
  import { isComputerCallOutputMessage } from '@/utils/toolContent';
33
+ import { INTENT_ARG } from '@/tools/intentArg';
34
+
35
+ /** Parses a stringified JSON object arg; undefined for anything else. */
36
+ function parseStringifiedArgsObject(
37
+ value: string
38
+ ): Record<string, unknown> | undefined {
39
+ if (!value.trim().startsWith('{')) {
40
+ return undefined;
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(value) as unknown;
44
+ if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
45
+ return parsed as Record<string, unknown>;
46
+ }
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ return undefined;
51
+ }
33
52
 
34
53
  /**
35
54
  * Non-global matcher for a single `{{tool<i>turn<n>}}` placeholder.
@@ -89,9 +108,20 @@ export type ResolveResult<T> = {
89
108
  * point in time, ignoring any subsequent registrations.
90
109
  */
91
110
  export interface ToolOutputResolveView {
92
- resolve<T>(args: T): ResolveResult<T>;
111
+ resolve<T>(args: T, options?: ResolveOptions): ResolveResult<T>;
93
112
  }
94
113
 
114
+ /**
115
+ * Per-call resolution options. `substituteIntentKey` opts the top-level
116
+ * `intent` key back INTO placeholder substitution: the exemption protects
117
+ * the injected display label, but a tool whose own schema declares a
118
+ * business parameter named `intent` (the injectors skip such tools) still
119
+ * needs references piped into it like any other argument.
120
+ */
121
+ export type ResolveOptions = {
122
+ substituteIntentKey?: boolean;
123
+ };
124
+
95
125
  /**
96
126
  * Pre-resolved arg map keyed by `toolCallId`. Used by the mixed
97
127
  * direct+event dispatch path to feed event calls' resolved args
@@ -333,12 +363,16 @@ export class ToolOutputReferenceRegistry {
333
363
  * the serialized args, the original input is returned without
334
364
  * walking the tree.
335
365
  */
336
- resolve<T>(runId: string | undefined, args: T): ResolveResult<T> {
366
+ resolve<T>(
367
+ runId: string | undefined,
368
+ args: T,
369
+ options?: ResolveOptions
370
+ ): ResolveResult<T> {
337
371
  if (!hasAnyPlaceholder(args)) {
338
372
  return { resolved: args, unresolved: [] };
339
373
  }
340
374
  const bucket = this.runStates.get(this.keyFor(runId));
341
- return this.resolveAgainst(bucket?.entries ?? EMPTY_ENTRIES, args);
375
+ return this.resolveAgainst(bucket?.entries ?? EMPTY_ENTRIES, args, options);
342
376
  }
343
377
 
344
378
  /**
@@ -358,27 +392,52 @@ export class ToolOutputReferenceRegistry {
358
392
  ? new Map(bucket.entries)
359
393
  : EMPTY_ENTRIES;
360
394
  return {
361
- resolve: <T>(args: T): ResolveResult<T> =>
362
- this.resolveAgainst(entries, args),
395
+ resolve: <T>(args: T, options?: ResolveOptions): ResolveResult<T> =>
396
+ this.resolveAgainst(entries, args, options),
363
397
  };
364
398
  }
365
399
 
366
400
  private resolveAgainst<T>(
367
401
  entries: ReadonlyMap<string, string>,
368
- args: T
402
+ args: T,
403
+ options?: ResolveOptions
369
404
  ): ResolveResult<T> {
370
405
  if (!hasAnyPlaceholder(args)) {
371
406
  return { resolved: args, unresolved: [] };
372
407
  }
408
+ const exemptIntentKey = options?.substituteIntentKey !== true;
373
409
  const unresolved = new Set<string>();
374
- const resolved = this.transform(entries, args, unresolved) as T;
410
+ /**
411
+ * Providers may deliver the args OBJECT as a JSON string. A plain
412
+ * string-root transform would expand placeholders inside the `intent`
413
+ * label too, bypassing the top-level exclusion below — parse, transform
414
+ * key-aware, and re-serialize so the label stays verbatim while every
415
+ * other field still substitutes. Only taken when an `intent` key is
416
+ * actually present; other strings keep the fast raw-string path.
417
+ */
418
+ if (exemptIntentKey && typeof args === 'string') {
419
+ const parsedRoot = parseStringifiedArgsObject(args);
420
+ if (parsedRoot != null && INTENT_ARG in parsedRoot) {
421
+ const resolved = JSON.stringify(
422
+ this.transform(entries, parsedRoot, unresolved, true)
423
+ ) as T;
424
+ return { resolved, unresolved: Array.from(unresolved) };
425
+ }
426
+ }
427
+ const resolved = this.transform(
428
+ entries,
429
+ args,
430
+ unresolved,
431
+ exemptIntentKey
432
+ ) as T;
375
433
  return { resolved, unresolved: Array.from(unresolved) };
376
434
  }
377
435
 
378
436
  private transform(
379
437
  entries: ReadonlyMap<string, string>,
380
438
  value: unknown,
381
- unresolved: Set<string>
439
+ unresolved: Set<string>,
440
+ exemptRootIntent = false
382
441
  ): unknown {
383
442
  if (typeof value === 'string') {
384
443
  return this.replaceInString(entries, value, unresolved);
@@ -390,7 +449,16 @@ export class ToolOutputReferenceRegistry {
390
449
  const source = value as Record<string, unknown>;
391
450
  const next: Record<string, unknown> = {};
392
451
  for (const [key, item] of Object.entries(source)) {
393
- next[key] = this.transform(entries, item, unresolved);
452
+ /**
453
+ * The top-level `intent` arg is a display label, never a data
454
+ * channel: expanding a `{{tool<i>turn<n>}}` placeholder there would
455
+ * dump a stored output (up to the registry cap) into a single-line
456
+ * UI label and persist it with the message. Left verbatim instead.
457
+ */
458
+ next[key] =
459
+ exemptRootIntent && key === INTENT_ARG
460
+ ? item
461
+ : this.transform(entries, item, unresolved);
394
462
  }
395
463
  return next;
396
464
  }
@@ -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 = {